from os.path import join as pjoin

import matplotlib.pyplot as plt
import numpy as np
import obspy
from matplotlib.patches import Rectangle
from obspy import UTCDateTime as utct
from tqdm import tqdm

from mqs_reports.catalog import Catalog
from mqs_reports.event import EVENT_TYPES_PRINT
from mqs_reports.utils import create_ZNE_HG, remove_sensitivity_stable, solify

SECONDS_PER_DAY = 86400.

class Noise:
    def __init__(self,
                 data: dict = None,
                 sc3_dir: str = None,
                 starttime: obspy.UTCDateTime = None,
                 endtime: obspy.UTCDateTime = None,
                 inv: obspy.Inventory = None,
                 winlen_sec: float = None,
                 fmin_LF: float = 1. / 6.,
                 fmax_LF: float = 1. / 1.5,
                 fmin_HF: float = 2.2,
                 fmax_HF: float = 2.6,
                 fmin_HF_broad: float = 1.2,
                 fmax_HF_broad: float = 3.0,
                 fmin_press: float = 1. / 12.,
                 fmax_press: float = 1. / 1.5
                 ):
        self.sols_quant = None
        if data is None:
            self.sc3_dir = sc3_dir
            self.winlen_sec = winlen_sec

            self.stds_HF_broad = 0.
            self.stds_HF = 0.
            self.stds_LF = 0.
            self.stds_press = 0.
            self.times = 0.
            self.times_LMST = 0.
            self.sol = 0.
            self.fmin_LF = fmin_LF
            self.fmax_LF = fmax_LF
            self.fmin_HF = fmin_HF
            self.fmax_HF = fmax_HF
            self.fmin_HF_broad = fmin_HF_broad
            self.fmax_HF_broad = fmax_HF_broad
            self.fmin_press = fmin_press
            self.fmax_press = fmax_press
            self._add_data(starttime=starttime,
                           endtime=endtime,
                           inv=inv)

        else:
            self.stds_HF = np.asarray(data['stds_HF'])
            self.stds_LF = np.asarray(data['stds_LF'])
            self.stds_HF_broad = np.asarray(data['stds_HF_broad'])
            self.stds_press = np.asarray(data['stds_press'])
            self.times = np.asarray(data['times'])
            self.times_LMST = np.asarray(data['times_LMST'])
            self.sol = np.asarray(data['sol'])
            self.fmin_LF = data['freqs'][0]
            self.fmax_LF = data['freqs'][1]
            self.fmin_HF = data['freqs'][2]
            self.fmax_HF = data['freqs'][3]
            self.fmin_press = data['freqs'][4]
            self.fmax_press = data['freqs'][5]
            self.fmin_HF_broad = 1.2  # data['freqs'][6]
            self.fmax_HF_broad = 3.0  # data['freqs'][7]

    def __str__(self):
        fmt = 'Noise from %s to %s, time windows: %d(HF), %d(LF)'
        return fmt % (self.times[0].date, self.times[-1].date,
                      len(self.stds_LF), len(self.stds_HF))

    def _add_data(self,
                  starttime: obspy.UTCDateTime,
                  endtime: obspy.UTCDateTime,
                  inv: obspy.Inventory,
                  ):

        dirnam = pjoin(self.sc3_dir, 'op/data/waveform/%d/XB/ELYSE/B[LH]?.D')
        filenam_VBB_HG = 'XB.ELYSE.0[237].B[LH]?.D.%d.%03d'

        dirnam_pressure = pjoin(self.sc3_dir,
                                'op/data/waveform/%d/XB/ELYSE/?DO.D')

        jday_start = starttime.julday
        jday_end = int(float(endtime - starttime) / SECONDS_PER_DAY
                       + jday_start)
        year_start = starttime.year

        stds_HF = list()
        stds_LF = list()
        stds_press = list()
        stds_HF_broad = list()
        times = list()
        times_LMST = list()
        sol = list()
        print('reading noise seismic data from %s (%d-%d)' % (self.sc3_dir,
                                                              jday_start,
                                                              jday_end))
        for jday in tqdm(range(jday_start, jday_end)):
            year = year_start + (jday // 365)
            try:
                # TODO: This will fail in leap years
                fnam = pjoin(dirnam % year,
                             filenam_VBB_HG % (year, jday % 365))
                st = obspy.read(fnam)
            except Exception:
                print('did not find %s' % fnam)
                st = obspy.Stream()
            try:
                # Before switching to L2 continuous data
                if year == 2019 and jday < 120:
                    filenam_pressure = 'XB.ELYSE.02.MDO.D.%d.%03d'
                else:
                    filenam_pressure = 'XB.ELYSE.03.BDO.D.%d.%03d'
                # TODO: This will fail in leap years
                fnam_press = pjoin(dirnam_pressure % year,
                                   filenam_pressure % (year, jday % 365))
                st_press = obspy.read(fnam_press)
                if year > 2019 or jday > 120:
                    st_press.decimate(5)

            except Exception:
                print('did not find %s' % fnam)
                st_press = obspy.Stream()
            st.merge()
            if len(st.select(location='03')) == 3:
                st = st.select(location='03')
            elif len(st.select(location='02')) == 3:
                st = st.select(location='02')
            else:
                st = st.select(location='07')
            if len(st) == 3:
                for tr in st:
                    remove_sensitivity_stable(tr, inv)
                st = create_ZNE_HG(st, inv=inv)
                st = st.select(channel='B[HL]Z')
                st.filter('highpass', freq=1. / 10., corners=8)
                st.integrate()
                st.filter('highpass', freq=1. / 10., corners=8)

                st_filt_HF = st.copy()
                st_filt_HF.filter('highpass', freq=self.fmin_HF, corners=16)
                st_filt_HF.filter('lowpass', freq=self.fmax_HF, corners=16)

                st_filt_HF_broad = st.copy()
                st_filt_HF_broad.filter('highpass', freq=self.fmin_HF_broad,
                                        corners=16)
                st_filt_HF_broad.filter('lowpass', freq=self.fmax_HF_broad,
                                        corners=16)

                st_filt_LF = st.copy()
                st_filt_LF.filter('highpass', freq=self.fmin_LF, corners=16)
                st_filt_LF.filter('lowpass', freq=self.fmax_LF, corners=16)

                for tr in st_press:
                    remove_sensitivity_stable(tr, inv=inv)
                st_press.detrend()
                st_press.taper(max_length=10., max_percentage=1.)
                st_press.merge(method=0, fill_value='interpolate')
                st_press.filter('highpass', freq=self.fmin_press, corners=16)
                st_press.filter('highpass', freq=self.fmin_press, corners=16)
                st_press.filter('lowpass', freq=self.fmax_press, corners=16)

                for t in np.arange(0, SECONDS_PER_DAY, self.winlen_sec):
                    t0 = utct('%04d%03d' % (year, jday % 365)) + t
                    t1 = t0 + self.winlen_sec
                    st_win = st.slice(starttime=t0, endtime=t1)
                    st_filt_HF_broad_win = st_filt_HF_broad.slice(
                                                      starttime=t0,
                                                      endtime=t1)
                    st_filt_HF_win = st_filt_HF.slice(starttime=t0,
                                                      endtime=t1)
                    st_filt_LF_win = st_filt_LF.slice(starttime=t0,
                                                      endtime=t1)

                    st_press_win = st_press.slice(starttime=t0, endtime=t1)
                    if len(st_win) > 0 and st_win[0].stats.npts > 10:
                        std_HF_broad = st_filt_HF_broad_win[0].std()
                        std_HF = st_filt_HF_win[0].std()
                        std_LF = st_filt_LF_win[0].std()

                        if len(st_press_win) > 0 and \
                                st_press_win[0].stats.npts > 10:
                            std_press = st_press_win[0].std()
                        else:
                            std_press = 0.

                        stds_press.append(std_press)
                        stds_HF_broad.append(std_HF_broad)
                        stds_HF.append(std_HF)
                        stds_LF.append(std_LF)
                        t0_lmst = solify(t0)

                        times.append(t0)
                        times_LMST.append(float(t0_lmst) % SECONDS_PER_DAY)
                        sol.append(1 + int(float(t0_lmst) / SECONDS_PER_DAY))

        self.stds_HF_broad = np.asarray(stds_HF_broad)
        self.stds_HF = np.asarray(stds_HF)
        self.stds_LF = np.asarray(stds_LF)
        self.stds_press = np.asarray(stds_press)
        self.times = np.asarray(times)
        self.times_LMST = np.asarray(times_LMST)
        self.sol = np.asarray(sol)

    def select(self,
               starttime: utct = None,
               endtime: utct = None,
               ):
        """
        Return new Catalog object only with the events that match the given
        criteria (e.g. all with name=="S026?a").
        Criteria can either be given as string with wildcards or as tuple of
        allowed values.
        :param starttime: minimum origin time (in UTC)
        :param endtime: maximum origin time (in UTC)
        :return:
        """
        import copy

        self_new = copy.deepcopy(self)

        bol = np.array((self.times > utct(starttime),
                        self.times < utct(endtime))
                       ).all(axis=0)
        self_new.stds_HF_broad = self.stds_HF_broad[bol]
        self_new.stds_HF = self.stds_HF[bol]
        self_new.stds_LF = self.stds_LF[bol]
        self_new.stds_press = self.stds_press[bol]
        self_new.times_LMST = self.times_LMST[bol]
        self_new.sol = self.sol[bol]
        self_new.times = self.times[bol]

        return self_new

    def save(self, fnam):
        np.savez(fnam,
                 stds_HF_broad=self.stds_HF_broad,
                 stds_HF=self.stds_HF,
                 stds_LF=self.stds_LF,
                 stds_press=self.stds_press,
                 times=self.times,
                 times_LMST=self.times_LMST,
                 sol=self.sol,
                 freqs=[self.fmin_LF, self.fmax_LF,
                        self.fmin_HF, self.fmax_HF,
                        self.fmin_press, self.fmax_press,
                        self.fmin_HF_broad, self.fmax_HF_broad],
                 winlen_sec=self.winlen_sec)

    def save_ascii(self, fnam):
        with open(fnam, 'w') as fid:
            fid.write(f"{'time (UTC)':>20s}, {'time (LMST)':>11s}, "
                      f"{'std_LF':>10s}, {'std_HF':>10s}, "
                      f"{'std_broad':>10s}, {'std_press':>10s}"
                      f"\n")
            for time, sol, time_LMST, std_LF, std_HF, std_press, std_broad in \
                    zip(self.times, self.sol, self.times_LMST, self.stds_LF,
                        self.stds_HF, self.stds_press, self.stds_HF_broad):
                t = utct(time)
                t_LMST = utct(time_LMST)
                for s in (std_LF, std_HF, std_press, std_broad):
                    if s < 1e-15:
                        s = 0.0

                fid.write(f"{t.strftime('%Y-%m-%dT:%H:%M:%S'):20s}, "
                          f"{sol:03d}M{t_LMST.strftime('%H:%M:%S'):8s}, "
                          f"{std_LF:10.4e}, {std_HF:10.4e}, "
                          f"{std_broad:10.4e}, {std_press:10.4e}\n")

    def plot_noise_stats(self, sol_start=80, sol_end=None,
                         ax=None, show=True):
        power_bins, p_LF, p_HF = self.calc_noise_stats(sol_end,
                                                       sol_start)

        if ax is None:
            fig, ax = plt.subplots(1, 1)

        ax.plot(power_bins, p_LF,
                label='LF, 1.5 - 6 seconds')
        ax.plot(power_bins, p_HF,
                label='HF, 2 - 3 Hz')
        plt.legend()

        if show:
            plt.show()

    def calc_noise_stats(self, sol_end=None, sol_start=80):
        if sol_end is None:
            # Now
            sol_end = float(solify(utct())) // SECONDS_PER_DAY
        binwidth = 2.
        bins = np.arange(-260, -120, binwidth)
        bol_LF = np.array([np.isfinite(self.stds_LF),
                           self.sol > sol_start,
                           self.sol < sol_end]).all(axis=0)
        power_LF, bins_tmp = np.histogram(
            20 * np.log10(self.stds_LF[bol_LF]),
            bins=bins, density=True)
        bol_HF = np.array([np.isfinite(self.stds_HF),
                           self.sol > sol_start,
                           self.sol < sol_end]).all(axis=0)
        power_HF, bins_tmp = np.histogram(
            20 * np.log10(self.stds_HF[bol_HF]),
            bins=bins, density=True)
        bins = bins[0:-1] + binwidth / 2.
        p_LF = np.cumsum(power_LF) * binwidth
        p_HF = np.cumsum(power_HF) * binwidth
        return bins, p_LF, p_HF

    def plot_daystats(self, cat: Catalog = None,
                      sol_start: int = 80, sol_end: int = 500, data_apss=False,
                      fnam_out='noise_vs_eventamplitudes.png', extra_data=None,
                      grouping='quantiles', color_scheme='standard',
                      cmap_dist='plasma_r', tau_data=None, metal=False):

        def make_patch_spines_invisible(ax):
            ax.set_frame_on(True)
            ax.patch.set_visible(False)
            for sp in ax.spines.values():
                sp.set_visible(False)

        if self.sols_quant is None:
            if grouping == 'quantiles':
                self.calc_quantiles(sol_end, sol_start,
                                    qs=(0.10, 0.33, 0.67, 0.9))
            elif grouping == 'timewindows':
                self.calc_time_windows(sol_end, sol_start)
                if metal:
                    labels = ['quiet\n(17-22 LMST)',
                              'NÖCTURNAL WAVEGÜIDE\n(22-5 LMST)',
                              'SÜNRÏSE\n(5-9 LMST)',
                              'SËISMIC NÖISË\n(9-17 LMST)']
                else:
                    labels = ['quiet\n(17-22 LMST)',
                              'noisy night\n(22-5 LMST)',
                              'morning\n(5-9 LMST)',
                              'day\n(9-17 LMST)']
            else:
                raise ValueError(f'Unknown value for grouping: {grouping}')

            self.save_quantiles(fnam=f'noise_{grouping}.npz')
        if grouping == 'quantiles':
            labels = ['10%', '33%', '67%', '90%']
        elif grouping == 'timewindows':
            if metal:
                labels = ['quiet\n(17-22 LMST)',
                          'NÖCTURNAL WAVEGÜIDE\n(22-5 LMST)',
                          'SÜNRÏSE\n(5-9 LMST)',
                          'SËISMIC NÖISË\n(9-17 LMST)']
            else:
                labels = ['quiet\n(17-22 LMST)', 'noisy night\n(22-5 LMST)',
                          'morning\n(5-9 LMST)', 'day\n(9-17 LMST)']

        fig = plt.figure(figsize=(16, 9))
        # ax_HF = ax[1
        # ax_LF = ax[0]
        h_base = 0.09
        w_base = 0.05
        w_LF = 0.85
        h_pad = 0.06
        h_LF = (0.97 - h_base - h_pad) / 2.

        if data_apss:
            h_apss = 0.2
            h_LF = (0.97 - h_base - 2 * h_pad - h_apss) / 2.
            # h_LF -= h_apss / 2. + h_pad

        ax_HF = fig.add_axes([w_base, h_base,
                              w_LF, h_LF])
        if data_apss:
            ax_apss = fig.add_axes([w_base, h_base + h_LF * 2 + h_pad * 2,
                                    w_LF, h_apss],
                                   sharex=ax_HF)
            if extra_data is not None:
                ax_extra = ax_apss.twinx()
                m = 's'
                ax_extra.plot(extra_data[0], extra_data[1],
                              markeredgecolor='k', markerfacecolor='C0',
                              marker=m, ls=None, lw=0., ms=4.,
                              label='DuDes per Sol\n(A. Spiga)')
                ax_extra.set_yticks(np.arange(0, 90, 15))
                ax_extra.set_ylim(0, 90)
                ax_extra.set_ylabel('number of DuDes')
                ax_extra.yaxis.label.set_color('C0')
                ax_extra.tick_params(axis='y', colors='C0')
                ax_extra.legend(loc='upper left')
                if tau_data is not None:
                    ax_tau = ax_apss.twinx()
                    # Offset the right spine of par2.  The ticks and label have
                    # already been
                    # placed on the right by twinx above.
                    ax_tau.spines["right"].set_position(("axes", 1.05))
                    # Having been created by twinx, par2 has its frame off, so
                    # the line of its
                    # detached spine is invisible.  First, activate the frame
                    # but make the patch
                    # and spines invisible.
                    make_patch_spines_invisible(ax_tau)
                    # Second, show the right spine.
                    ax_tau.spines["right"].set_visible(True)
                    m = 'o'
                    ax_tau.plot(tau_data[0], tau_data[1],
                                markeredgecolor='k', markerfacecolor='orange',
                                marker=m, ls=None, lw=0., ms=6.,
                                label='tau\n(M. Lemmon)')
                    ax_tau.set_yticks(np.arange(0.3, 2.0, 0.3))
                    ax_tau.set_ylim(0.3, 1.8)
                    ax_tau.set_ylabel('tau')
                    ax_tau.yaxis.label.set_color('orange')
                    ax_tau.tick_params(axis='y', colors='orange')
                    ax_tau.legend()
        else:
            ax_apss = fig.add_axes([1.2, 1.2, 0.1, 0.1])

        ax_LF = fig.add_axes([w_base, h_base + h_LF + h_pad,
                              w_LF, h_LF],
                             sharex=ax_HF)

        cols = ['black', 'darkgrey', 'grey', 'black']
        ls = ['dotted', 'dashed', 'dotted', 'dashed']
        for i in range(self.quantiles_LF.shape[1] - 1, -1, -1):
            ax_LF.plot(self.sols_quant - 1.,
                       10 * np.log10(self.quantiles_LF[:, i]),
                       label=labels[i], c=cols[i], ls=ls[i])
        ax_LF.fill_between(x=self.sols_quant - 1.,
                           y1=-300,
                           y2=10 * np.log10(self.quantiles_LF[:, 0]),
                           facecolor='lightgrey')

        for i in range(self.quantiles_HF.shape[1] - 1, -1, -1):
            ax_HF.plot(self.sols_quant - 1.,
                       10 * np.log10(self.quantiles_HF[:, i]),
                       label=labels[i], c=cols[i], ls=ls[i])
        ax_HF.fill_between(x=self.sols_quant - 1.,
                           y1=-300,
                           y2=10 * np.log10(self.quantiles_HF[:, 0]),
                           facecolor='lightgrey')
        if data_apss:
            for i in range(self.quantiles_press.shape[1] - 1, -1, -1):
                ax_apss.plot(self.sols_quant - 1.,
                             10 * np.log10(self.quantiles_press[:, i]),
                             label=labels[i], c=cols[i], ls=ls[i])
            ax_apss.fill_between(x=self.sols_quant - 1.,
                                 y1=-300,
                                 y2=10 * np.log10(self.quantiles_press[:, 0]),
                                 facecolor='lightgrey')

        # Add one-year marker
        SOLS_PER_YEAR = 668
        for ax in [ax_LF, ax_HF]:
            ax.axvline(x=sol_start + SOLS_PER_YEAR, ls='dashed',
                       color='black', lw=1)
        if data_apss:
            ax_apss.axvline(x=sol_start + SOLS_PER_YEAR, ls='dashed',
                            color='black', lw=1)

        HF_times = []
        HF_amps = []
        HF_dists = []
        LF_times = []
        LF_amps = []
        LF_dists = []
        TF_times = []
        TF_amps = []
        TF_dists = []

        if color_scheme == 'standard':
            symbols = {'2.4_HZ': 'v',
                       'HIGH_FREQUENCY': '^',
                       'VERY_HIGH_FREQUENCY': '*',
                       'LOW_FREQUENCY': 's',
                       # 'ABOVE': 10,
                       'BROADBAND': 'D'}
        elif color_scheme == 'Knapmeyer':
            # Knapmeyer edition
            symbols = {'2.4_HZ': 'o',
                       'HIGH_FREQUENCY': 's',
                       'VERY_HIGH_FREQUENCY': 'v',
                       'LOW_FREQUENCY': 'o',
                       'BROADBAND': 'v'}
            cols = {'2.4_HZ':              [0.57, 0.86, 0.98],
                    'HIGH_FREQUENCY':      [0.57, 0.86, 0.98],
                    'VERY_HIGH_FREQUENCY': [0.57, 0.86, 0.98],
                    'LOW_FREQUENCY':       [0.45, 0.35, 0.22],
                    'BROADBAND':           [0.45, 0.35, 0.22]}
        else:
            raise ValueError('invalid argument for color_scheme: ' +
                             color_scheme)

        markers_HF = []
        markers_LF = []
        markers_TF = []

        if cat is not None:
            cmap = plt.cm.get_cmap(cmap_dist)
            cmap.set_under('lightgrey')
            for event in cat.select(event_type=['HF', 'VF']):
                markers_HF.append(symbols[event.mars_event_type])

                #  Plot HF events with high amplitudes at top of plot
                if event.amplitudes['A_24'] < -180:
                    HF_amps.append(event.amplitudes['A_24'])
                else:
                    HF_amps.append(-181)

                #  Make events without a distance lightgrey
                if event.distance is None:
                    HF_dists.append(-1)
                else:
                    HF_dists.append(event.distance)
                HF_times.append(float(solify(event.starttime)) /
                                SECONDS_PER_DAY)

            for event in cat.select(event_type=['24']):
                markers_TF.append(symbols[event.mars_event_type])
                if event.distance is None:
                    TF_dists.append(-1.)
                else:
                    TF_dists.append(event.distance)
                TF_times.append(float(solify(event.starttime)) /
                                SECONDS_PER_DAY)
                TF_amps.append(event.amplitudes['A_24'])

            for event in cat.select(event_type=['LF', 'BB']):
                markers_LF.append(symbols[event.mars_event_type])
                if event.distance is None:
                    LF_dists.append(-1.)
                else:
                    LF_dists.append(event.distance)
                amp_P = event.pick_amplitude(
                    pick='Peak_MbP',
                    comp='vertical',
                    fmin=self.fmin_LF,
                    fmax=self.fmax_LF,
                    instrument='VBB'
                    )
                amp_S = event.pick_amplitude(
                    pick='Peak_MbS',
                    comp='vertical',
                    fmin=self.fmin_LF,
                    fmax=self.fmax_LF,
                    instrument='VBB'
                    )
                amp = max(i for i in (amp_P, amp_S, 0.0) if i is not None)
                LF_times.append(float(solify(event.starttime)) /
                                SECONDS_PER_DAY)
                LF_amps.append(20 * np.log10(amp))

            for event_type, m in symbols.items():
                if event_type != 'ABOVE':
                    bol = np.array(markers_LF) == m
                    if len(bol) > 0:
                        if color_scheme == 'standard':
                            sc = ax_LF.scatter(x=np.array(LF_times)[bol],
                                               y=np.array(LF_amps)[bol],
                                               c=np.array(LF_dists)[bol],
                                               vmin=00., vmax=100., cmap=cmap,
                                               edgecolors='k', linewidths=0.5,
                                               s=80., marker=m, zorder=100)
                        else:
                            sc = ax_LF.scatter(x=np.array(LF_times)[bol],
                                               y=np.array(LF_amps)[bol],
                                               c=cols[event_type],
                                               edgecolors='k', linewidths=0.5,
                                               s=80., marker=m, zorder=100)

            cax = fig.add_axes([w_LF + w_base + 0.02,
                                h_base + 0.165,
                                0.018,
                                1 - h_base - 0.55])
            cb = plt.colorbar(sc, ax=ax_LF, cax=cax)
            cb.ax.set_ylabel('distance / degree', rotation=270.,
                             labelpad=4.45)
            for event_type, m in symbols.items():
                bol = np.array(markers_TF) == m
                if len(bol) > 0:
                    if color_scheme == 'standard':
                        sc = ax_HF.scatter(x=np.array(TF_times)[bol],
                                           y=np.array(TF_amps)[bol],
                                           c=np.array(TF_dists)[bol],
                                           vmin=00., vmax=100., cmap=cmap,
                                           edgecolors='k', linewidths=0.5,
                                           s=20., marker=m, zorder=8)
                    else:
                        sc = ax_HF.scatter(x=np.array(TF_times)[bol],
                                           y=np.array(TF_amps)[bol],
                                           c=cols[event_type],
                                           edgecolors='k', linewidths=0.5,
                                           s=20., marker=m, zorder=8)
            for event_type, m in symbols.items():
                bol = np.array(markers_HF) == m
                if len(bol) > 0:
                    if color_scheme == 'standard':
                        sc = ax_HF.scatter(x=np.array(HF_times)[bol],
                                           y=np.array(HF_amps)[bol],
                                           c=np.array(HF_dists)[bol],
                                           vmin=00., vmax=100., cmap=cmap,
                                           edgecolors='k', linewidths=0.5,
                                           s=40., marker=m, zorder=10)
                    else:
                        sc = ax_HF.scatter(x=np.array(HF_times)[bol],
                                           y=np.array(HF_amps)[bol],
                                           c=cols[event_type],
                                           edgecolors='k', linewidths=0.5,
                                           s=40., marker=m, zorder=10)

            # cax = plt.colorbar(sc, ax=ax_HF, use_gridspec=True,
            #                    fraction=0.08)
            # cax.ax.set_ylabel('distance / degree', rotation=270.,
            #                   labelpad=12.45)
            # if data_apss:
            #     cax = plt.colorbar(sc, ax=ax_apss, use_gridspec=True)
            #     cax.ax.set_ylabel('distance / degree', rotation=270.,
            #                       labelpad=12.45)

        for a in [ax_HF, ax_LF, ax_apss]:
            a.grid(True)
            rect = Rectangle(xy=(267, -300), width=20., height=400, zorder=10,
                             facecolor='darkgrey', edgecolor='black')
            a.add_patch(rect)

        sc = ax_HF.scatter(0, -300, label='Marsquake',
                           edgecolors='k', linewidths=0.5,
                           c='royalblue', s=80., marker='.')
        ax_HF.set_xlabel('Sol number')
        ax_LF.set_ylabel('PSD, displ. %3.1f-%3.1f sec. [dB]' %
                         (1. / self.fmax_LF, 1. / self.fmin_LF))
        ax_HF.set_ylabel('PSD, displ. %3.1f-%3.1f Hz. [dB]' %
                         (self.fmin_HF, self.fmax_HF))

        ax_LF.set_ylim(-210., -165.)
        ax_LF.set_title('Seismic power, low frequency,  ' +
                        '%3.1f-%3.1f seconds and LF/BB events' %
                        (1. / self.fmax_LF, 1. / self.fmin_LF))
        ax_HF.set_title('Seismic power, high frequency,  ' +
                        '%3.1f-%3.1f Hz and HF/2.4 Hz events' %
                        (self.fmin_HF, self.fmax_HF))
        ax_HF.set_ylim(-225., -180.)
        ax_LF.set_xlim(sol_start, sol_end)

        # ax_HF.text(0.7, -0.12, s=str(self),
        #            transform=ax_HF.transAxes)

        # Daytimes or quantiles legend
        l_LF = ax_LF.legend(bbox_to_anchor=(w_LF + w_base, 0.61),
                            loc='lower left',
                            bbox_transform=fig.transFigure, framealpha=1.0)
        l_LF.set_zorder(50)

        handles = []
        labels = []
        EVENT_TYPES_PRINT['VERY_HIGH_FREQUENCY'] = 'very high\nfrequency'
        EVENT_TYPES_PRINT['HIGH_FREQUENCY'] = 'high\nfrequency'
        for event_type, m in symbols.items():
            h, = ax_HF.plot(-100, -100, markeredgecolor='k',
                            markerfacecolor='white',
                            marker=m, ls=None, lw=0., ms=8.)
            handles.append(h)
            labels.append(EVENT_TYPES_PRINT[event_type])
        #  Event type legend
        l_HF = ax_HF.legend(handles=handles, labels=labels,
                            bbox_to_anchor=(w_LF + w_base, h_base - 0.01),
                            loc='lower left',
                            bbox_transform=fig.transFigure, framealpha=1.0)
        l_HF.set_zorder(50)

        if data_apss:
            ax_apss.set_title('Pressure power %3.1f-%3.1f seconds' %
                              (1. / self.fmax_press, 1. / self.fmin_press))
            ax_apss.set_ylabel('PSD, pressure.\n%3.1f-%4.1f sec. [dB]' %
                               (1. / self.fmax_press, 1. / self.fmin_press))
            ax_apss.set_ylim(-50, -20)

        plt.savefig(fnam_out, dpi=200)

    def read_quantiles(self, fnam):
        data = np.load(fnam)
        self.quantiles_HF = data['quantiles_HF']
        self.quantiles_LF = data['quantiles_LF']
        self.quantiles_press = data['quantiles_press']
        self.sols_quant = data['sols']

    def save_quantiles(self, fnam):
        np.savez(file=fnam,
                 quantiles_HF=self.quantiles_HF,
                 quantiles_LF=self.quantiles_LF,
                 quantiles_press=self.quantiles_press,
                 sols=self.sols_quant)

    def calc_quantiles(self, sol_end, sol_start, qs):

        self.sols_quant = np.arange(sol_start, sol_end + 1)
        self.quantiles_LF = np.zeros(
                (sol_end - sol_start + 1, len(qs)))
        self.quantiles_HF = np.zeros_like(self.quantiles_LF)
        self.quantiles_press = np.zeros_like(self.quantiles_LF)

        values_HF = np.zeros(len(qs))
        values_LF = np.zeros_like(values_HF)
        values_press = np.zeros_like(values_HF)

        i = 0
        print('Calculating noise quantiles')
        df_HF = self.fmax_HF - self.fmin_HF
        df_LF = self.fmax_LF - self.fmin_LF
        df_press = self.fmax_press - self.fmin_press
        for isol in tqdm(self.sols_quant):
            bol_sol = self.sol == isol - 1
            if sum(bol_sol) > 1:
                disp_LF = self.stds_LF[bol_sol] ** 2. / df_LF
                disp_HF = self.stds_HF[bol_sol] ** 2. / df_HF
                disp_press = self.stds_press[bol_sol] ** 2. / df_press
                values_LF = np.nanquantile(disp_LF, q=qs)
                values_HF = np.nanquantile(disp_HF, q=qs)
                values_press = np.nanquantile(disp_press, q=qs)

                self.quantiles_HF[i, :] = np.nan_to_num(values_HF)
                self.quantiles_LF[i, :] = np.nan_to_num(values_LF)
                self.quantiles_press[i, :] = np.nan_to_num(values_press)
            i += 1

        # Mask outliers
        self.quantiles_LF = np.ma.masked_less(self.quantiles_LF, value=1e-23)
        self.quantiles_HF = np.ma.masked_less(self.quantiles_HF, value=1e-23)
        self.quantiles_press = np.ma.masked_less(self.quantiles_press,
                                                 value=1e-6)
        self.quantiles_LF = np.ma.masked_greater(self.quantiles_LF, value=1e-8)
        self.quantiles_HF = np.ma.masked_greater(self.quantiles_HF, value=1e-8)
        self.quantiles_press = np.ma.masked_greater(self.quantiles_press,
                                                    value=1e-2)

    def calc_time_windows(self, sol_end, sol_start,
                          time_windows_hour=[[17, 22.0],
                                             [22.0, 5.0],
                                             [5.0, 9.0],
                                             [9.0, 17.]]):

        self.sols_quant = np.arange(sol_start, sol_end + 1)
        self.quantiles_LF = np.zeros(
            (sol_end - sol_start + 1, len(time_windows_hour)))
        self.quantiles_HF = np.zeros_like(self.quantiles_LF)
        self.quantiles_press = np.zeros_like(self.quantiles_LF)

        values_HF = np.zeros(len(time_windows_hour))
        values_LF = np.zeros_like(values_HF)
        values_press = np.zeros_like(values_HF)

        i = 0
        print('Calculating noise medians')
        df_HF = self.fmax_HF - self.fmin_HF
        df_LF = self.fmax_LF - self.fmin_LF
        df_press = self.fmax_press - self.fmin_press
        for isol in tqdm(self.sols_quant):
            bol_sol = self.sol == isol - 1
            times_this_sol = (self.times_LMST[bol_sol] % 1.) * 24.
            if sum(bol_sol) > 1:
                for iwindow, time_window in enumerate(time_windows_hour):
                    if time_window[0] < time_window[1]:
                        bol_hours = np.array((
                            (time_window[0] < times_this_sol),
                            (times_this_sol < time_window[1]))).all(axis=0)
                    else:
                        bol_hours = np.array((
                            (time_window[0] < times_this_sol),
                            (times_this_sol < time_window[1]))).any(axis=0)

                    disp_LF = self.stds_LF[bol_sol] ** 2. / df_LF
                    disp_HF = self.stds_HF[bol_sol] ** 2. / df_HF
                    disp_press = self.stds_press[bol_sol] ** 2. / df_press
                    values_LF[iwindow] = np.nanmedian(disp_LF[bol_hours])
                    values_HF[iwindow] = np.nanmedian(disp_HF[bol_hours])
                    values_press[iwindow] = np.nanmedian(disp_press[bol_hours])

                self.quantiles_HF[i, :] = np.nan_to_num(values_HF)
                self.quantiles_LF[i, :] = np.nan_to_num(values_LF)
                self.quantiles_press[i, :] = np.nan_to_num(values_press)
            i += 1

        # Mask outliers
        self.quantiles_LF = np.ma.masked_less(self.quantiles_LF, value=1e-23)
        self.quantiles_HF = np.ma.masked_less(self.quantiles_HF, value=1e-23)
        self.quantiles_press = np.ma.masked_less(self.quantiles_press,
                                                 value=1e-6)
        self.quantiles_LF = np.ma.masked_greater(self.quantiles_LF, value=1e-8)
        self.quantiles_HF = np.ma.masked_greater(self.quantiles_HF, value=1e-8)
        self.quantiles_press = np.ma.masked_greater(self.quantiles_press,
                                                    value=1e-2)

    def compare_events(self,
                       catalog=None,
                       threshold_dB: float = 3.):
        ratios = []
        for event in tqdm(catalog.select(event_type=['24', 'HF', 'VF'])):
            nwins_below = sum(event.amplitudes['A_24'] >
                              threshold_dB + 20 * np.log10(self.stds_HF))
            event.ratio = nwins_below / len(self.stds_HF)
            ratios.append(event.ratio)

        for event in tqdm(catalog.select(event_type=['LF', 'BB'])):
            amp_P = event.pick_amplitude(
                pick='Peak_MbP',
                comp='vertical',
                fmin=self.fmin_LF,
                fmax=self.fmax_LF,
                instrument='VBB'
                )
            amp_S = event.pick_amplitude(
                pick='Peak_MbS',
                comp='vertical',
                fmin=self.fmin_LF,
                fmax=self.fmax_LF,
                instrument='VBB'
                )
            if amp_S is None:
                amp = amp_P
            elif amp_P is None:
                amp = amp_S
            else:
                amp = max((amp_P, amp_S))
            if amp is not None:
                nwins_below = sum(20 * np.log10(amp) >
                                  threshold_dB + 20 * np.log10(self.stds_LF))
                event.ratio = nwins_below / len(self.stds_LF)
            else:
                event.ratio = None
            ratios.append(event.ratio)
        # import matplotlib.pyplot as plt

        # plt.hist(ratios, bins=20)
        # plt.show()

def read_noise(fnam):
    data = np.load(fnam, allow_pickle=True)
    data_read = dict()
    for name in data.files:
        data_read[name] = data[name]

    return Noise(data=data_read)

if __name__ == '__main__':
    sc3_path = '/mnt/mnt_sc3data'
    inv = obspy.read_inventory('mqs_reports/data/inventory.xml')
    # noise = Noise(sc3dir=sc3_path,
    #               starttime=utct('20190202'),
    #               endtime=utct('20191026'),
    #               inv=inv,
    #               winlen_sec=120.
    #               )
    # noise.save('noise_0301_1025.npz')
    noise = read_noise('noise_0301_1025.npz')
    noise.plot_noise_stats()

    cat = Catalog(fnam_quakeml='mqs_reports/data/catalog_20191024.xml',
                  quality=['A', 'B'])

    cat.load_distances(fnam_csv='./mqs_reports/data/manual_distances.csv')
    cat.read_waveforms(inv=inv, sc3dir=sc3_path)
    cat.calc_spectra(winlen_sec=10.)
    noise.plot_daystats(cat, sol_start=200, sol_end=300)
    noise.compare_events(cat)
    noise.plot_noise_stats()

def calc_Ls(t_LMST):
    import marstime
    from mqs_reports.utils import UTCify
    t = float(UTCify(t_LMST)) * 1e3

    j2000_offset = marstime.j2000_offset_tt(
        marstime.julian_tt(marstime.julian(t)))
    ls = marstime.Mars_Ls(j2000_ott=j2000_offset)  # + 30 * 86400e3))))
    return ls


from datetime import date

class Goldfish:
    def __init__(self , name , species):
        self.name= name,
        self.species= species,
        self.date_added= date.today()
        self.swimming= True
        self.pond= True

goldie= Goldfish("Goldie", "fish")

# goldfishclass= Goldfish()
# print(goldfishclass.name)


from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time
import requests

links_list = ['rahvaraamat.ee/search/productList/en?offset=0&limit=10&searchTerm={}', 'bookvoed.ee/search?q={}&count=50#page-1', 'mnogoknig.ee/search/{}']

def createURL(keywordsList,urlExample): # returns a serach url with user keywords
    """ NOTICE!  white fang  - there is a book title example (which user will write in the form as a keyword)
        urlExample -             rahvaraamat.ee/search/productList/en?offset=0&limit=10&searchTerm={} - where {} there are search keywords
        function will return =>  rahvaraamat.ee/search/productList/en?offset=0&limit=10&searchTerm=мартин+иден  - instead of  {} will paste search keywords
                                 bookvoed.ee/search?q={}&count=50#page-1 =>  bookvoed.ee/search?q=white+fang
                                 mnogoknig.ee/search/{}  =>   mnogoknig.ee/search/white+fang

    """
    plusKeywords = '+'.join(keywordsList)
    url = 'https://' + urlExample.replace("{}",plusKeywords)
    return url

def item_url(page,class1,secondClass=None): # returns list of links for each item in search page.
    #.title > a.js-link-product     - rahvaraamat
    #.o-row > a.title               - bookvoed
    #.col-xs-8 > a                  - mnogoknig
    page = requests.get(page)
    soup = BeautifulSoup(page.content, 'html.parser')
    links = []

    if secondClass == None:
        string = f".{class1} > a"
    else:
        string = f".{class1} > a.{secondClass}"

    for a in soup.select(string):
        href = urljoin(page.url, a.get('href'))
        links.append(href)

    return links

def item_price(page,class1,secondClass=None): # returns list of prices for each item in search page
    # .meta > p.price - rahvaraamat
    # .buy > span     - bookvoed
    # .price          -mnogoknig
    page = requests.get(page)
    soup = BeautifulSoup(page.content, 'html.parser')
    prices = []
    if secondClass == None:
        string = f".{class1}"
    elif secondClass == 'span':
        string = f".{class1} > {secondClass}"
    elif secondClass == 'price':
        string = f".{class1} > p.{secondClass}"

    for p in soup.select(string):
        p =  p.get_text()
        prices.append(p)
    prices = [i.partition(' €')[0] for i in prices] # delete unnecessary symbols
    prices = [i.strip() for i in prices] # delete whitespaces
    prices = [i.replace('€','') for i in prices] # delete unnecessary symbols
    return prices

def clean_dict(dict1):
    copy = dict1.copy()
    for k, v in copy.items():
        if v == 'Out of stock':
            del dict1[k]
        elif v == 'This product is not available on e-store':
            del dict1[k]
    return dict1


# my solution
from collections import defaultdict

def solution(clothes):
    table = defaultdict(list)
    for cloth, name in clothes:
        table[name].append(cloth)

    n = len(table)
    # 옷 종류의 개수. 최소1개부터 최대n개까지 착용가능

    from itertools import combinations
    res = 0
    for i in range(1, n + 1):
        comb = combinations(table.keys(), i)
        for set in comb:
            temp = 1
            for j in range(i):
                temp *= len(table[set[j]])
            res += temp

    return res
# 30개의 tc 중 1개가 타임아웃이 난다.

def solution(clothes):
    table = defaultdict(list)
    for cloth, name in clothes:
        table[name].append(cloth)

    ans = 1
    for cloth in table.values():
        ans *= (len(cloth) + 1)

    return ans - 1
# Intuition: 안입는 경우도 한가지 경우로 생각해서 value 개수 + 1 을 다 곱해준다.
#            그리고 전부 다 안입은 경우을 제외하기 위해 1을 빼주면 완성

# 다른 사람의 코드
import collections
from functools import reduce

def solution(c):
    return reduce(lambda x,y:x*y,[a+1 for a in collections.Counter([x[1] for x in c]).values()])-1
# 우와우 사실상 원리는 내것과 똑같음. 람다와 reduce를 좀 더 연습해야겠다.


import pandas as pd
import torch
import torch.nn.functional as F
import numpy as np
import Func
import NetDefine
import matplotlib.pyplot as plt

if __name__ == '__main__':
    df = pd.read_csv(r'D:\codeProject\PycharmProject\DNN_Infection\Dataset\test.csv',
                     names=['preg', 'plas', 'pres', 'skin', 'test', 'mass', 'pedi', 'age', 'class'])

    X1 = df['preg']
    X2 = df['plas']
    X3 = df['pres']
    X4 = df['skin']
    X5 = df['test']
    X6 = df['mass']
    X7 = df['pedi']
    X8 = df['age']

    X1 = Func.Normalization(X1)
    X2 = Func.Normalization(X2)
    X3 = Func.Normalization(X3)
    X4 = Func.Normalization(X4)
    X5 = Func.Normalization(X5)
    X6 = Func.Normalization(X6)
    X7 = Func.Normalization(X7)
    X8 = Func.Normalization(X8)

    # print(X1, X2, X3, X4, X5, X6, X7, X8)

    X_df = [X1, X2, X3, X4, X5, X6, X7, X8]
    X = np.array(X_df).T
    X = X.tolist()
    X = torch.FloatTensor(X)
    # print(X)

    Y1 = df['class']

    # Y1 = Func.Normalization(Y1)

    Y_df = [Y1]
    Y = np.array(Y_df).T
    Y = Y.tolist()
    Y = torch.FloatTensor(Y)

    # print(Y)
    netD = NetDefine.TwoLayerNet(8, 10, 1)
    state_dict = torch.load('model.pt')
    netD.load_state_dict(state_dict['model'])
    input = X
    output = netD(input)
    # print(output)

    result = output.data.numpy()

    for i in range(len(result)):
        if result[i]<-0.5:
            result[i] = 0
        else:result[i] = 1

    Func.Calculation(result, Y1)

    x = np.linspace(0,153,154)

    plt.figure()
    plt.scatter(x, Y, label='class', s=10)
    plt.scatter(x, result , label='class_pre', s=10)
    plt.legend()

    plt.show()


import time
import json
import numpy as np
import csv
from utils import run_cmd
from params import WSK_CLI, COUCH_LINK

def invoke(function_id, param):
    try:
        cmd = '{} action invoke {} -b {} | grep -v "ok:"'.format(WSK_CLI, function_id, param)
        response = str(run_cmd(cmd))
        doc = json.loads(response)

        if len(doc["annotations"]) == 6:
            init_time = doc["annotations"][5]["value"]
        else:
            init_time = 0
        wait_time = doc["annotations"][1]["value"]
        duration = doc["duration"]
    except Exception:
        init_time = 0
        wait_time = 0
        duration = 0

    return init_time, wait_time, duration

def cold_start(interval, cold_loop_time, function_id, param):
    result = {}
    result["init_time"] = []
    result["wait_time"] = []
    result["duration"] = []
    result["completion_time"] = []

    for _ in range(cold_loop_time):
        time.sleep(interval)
        init_time, wait_time, duration = invoke(function_id, param)
        completion_time = init_time + wait_time + duration
        if completion_time > 0:
            result["init_time"].append(init_time)
            result["wait_time"].append(wait_time)
            result["duration"].append(duration)
            result["completion_time"].append(completion_time)

    time.sleep(interval)

    return result

def warm_start(interval, alloc_range, warm_loop_time, function_id, param):
    result = ["actual_id","init_time", "wait_time", "duration", "completion_time"]
    init_time_list = []
    wait_time_list  = []
    duration_list  = []
    completion_time_list  = []

    for i in range(alloc_range):
        actual_id = "{}_{}".format(function_id, i)
        for loop in range(warm_loop_time):
            init_time, wait_time, duration = invoke(actual_id, param)
            completion_time = init_time + wait_time + duration
            if loop > 0 and completion_time > 0:
                init_time_list.append(init_time)
                wait_time_list.append(wait_time)
                duration_list.append(duration)
                completion_time_list.append(completion_time)

        result.append(actual_id)
        result.append(0 if len(init_time_list) == 0 else np.average(init_time_list))
        result.append(0 if len(wait_time_list) == 0 else np.average(wait_time_list))
        result.append(0 if len(duration_list) == 0 else np.average(duration_list))
        result.append(0 if len(completion_time_list) == 0 else np.average(completion_time_list))

    time.sleep(interval)

    return result

def characterize_functions(
    interval,
    alloc_range,
    cold_loop_time,
    warm_loop_time,
    file_path,
    function_invoke_params
):
    for function_id in function_invoke_params.keys():
        param = function_invoke_params[function_id]
        # cold_result = cold_start(interval, cold_loop_time, function_id, param)
        warm_result = warm_start(interval, alloc_range, warm_loop_time, function_id, param)

        # Log warm starts
        with open(file_path + "{}_saturation_point.csv".format(function_id), "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerows(warm_result)

if __name__ == "__main__":
    interval = 1
    alloc_range = 64
    cold_loop_time = 10
    warm_loop_time = 1
    file_path = "logs/"

    function_invoke_params = {}
    # function_invoke_params["dh"] = "-p username hanfeiyu -p size 500000 -p parallel 128 "
    function_invoke_params["eg"] = ""
    # function_invoke_params["ip"] = "-p width 1000 -p height 1000 -p size 128 -p parallel 8 "
    # function_invoke_params["vp"] = "-p duration 10 -p size 1 -p parallel 1 "
    # function_invoke_params["ir"] = "-p size 1 -p parallel 1 "
    # function_invoke_params["knn"] = "-p dataset_size 100 -p feature_dim 80 -p k 3 -p size 24 -p parallel 1 "
    # function_invoke_params["alu"] = "-p size 100000000 -p parallel 64 "
    # function_invoke_params["ms"] = "-p size 1000000 -p parallel 8 "
    # function_invoke_params["gd"] = "-p x_row 20 -p x_col 20 -p w_row 40 -p size 8 -p parallel 8 "
    # function_invoke_params["dv"] = "-p size 5000 -p parallel 8 "

    for function_id in function_invoke_params.keys():
        function_invoke_params[function_id] = function_invoke_params[function_id] + "-p couch_link {} -p db_name {}".format(COUCH_LINK, function_id)

    characterize_functions(
        interval=interval,
        alloc_range=alloc_range,
        cold_loop_time=cold_loop_time,
        warm_loop_time=warm_loop_time,
        file_path=file_path,
        function_invoke_params=function_invoke_params
    )

    print("")
    print("Workload characterization finished!")
    print("")


# Importing Libraries
import numpy as np
import cv2

# Trained Classifier
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier('haarcascade_eye_tree_eyeglasses.xml')

cap = cv2.VideoCapture('test.mp4')

while cap.isOpened():
    ret, img = cap.read()

    if ret is True:
        gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
        faces = face_cascade.detectMultiScale(gray, 1.1, 4)

        for x, y, w, h in faces:
            cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 3)
            roi_eye_gray = gray[y:y+h, x:x+w]
            roi_eye_color = img[y:y+h, x:x+w]
            eyes = eye_cascade.detectMultiScale(roi_eye_gray)
            for ex, ey, ew, eh in eyes:
                cv2.rectangle(roi_eye_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 5)

        cv2.imshow('img', img)

        if cv2.waitKey(1) & 0xFF == 27:
            break
    else:
        break

cap.release()
cv2.destroyAllWindows()


"""
    This module contains generic creation of bracket
    slots functions.
"""

import math
import random
import time

from bracketool.brackets import brackets_depth_distance
from bracketool.brackets import generate_first_round_clashes
from bracketool.domain import Clash, ClashGenerator
from bracketool.teambrackets import clashes_team_count
from bracketool.teambrackets import create_reserved_teams_bracket_clashes

class PairingsGenerator(object):
    """Creates single elimination brackets."""

    def __init__(self, use_teams=True, use_rating=True, random_seed=None):
        if not random_seed:
            random_seed = time.time()
        self.rnd = random.Random(random_seed)
        self.use_teams = use_teams
        self.use_rating = use_rating

    def _find_competitor_clash_options(self, competitor, reservations, clashes):
        """Find the available clash options for a competitor, based on
        reservations made for a team.

        :param competitor: A competitor
        :param reservations: List of team reservations for each clash:
            [[team_a, team_b], [team_c,], [], [team_b,]]
            The length of reservation matches MUST match the length of the
            first round clashes.
            If there are no reservations, it MUST be a list of empty lists.
        :param clashes: List of Clashes for the all the rounds.

        :returns: a list of indices where a competitor can be placed.
        """
        options = []
        # find if we have reserved spots for this team:
        team_count = clashes_team_count(reservations)
        if competitor.team in team_count:
            for idx, reserv in enumerate(reservations):
                if competitor.team in reserv and clashes[idx].has_spot():
                    options.append(idx)
            if options:
                return options
        # if we could not find a reserved spot, use a free spot
        for idx, reserv in enumerate(reservations):
            if len(reserv) == 2:
                # skip full reseved
                continue
            elif len(reserv) == 1:
                # skip if there is one reservation, and only one spot left
                if clashes[idx].is_bye:
                    continue
                if clashes[idx].competitor_a is not None:
                    continue
            if clashes[idx].has_spot():
                options.append(idx)
        return options

    def _assign_clash(self, competitor, clashes, clash_idx, reservations,
                      team_pairing_count):
        clash = clashes[clash_idx]
        clash.add_competitor(competitor)
        if reservations and competitor.team in reservations[clash_idx]:
            # remove the reservations, because we are assigning it
            reservations[clash_idx].remove(competitor.team)
        if not clash.has_spot() and not clash.is_bye and \
                team_pairing_count is not None:
            team_a = clash.competitor_a.team
            team_b = clash.competitor_b.team
            if team_a and team_b:
                pt = (min(team_a, team_b), max(team_a, team_b))
                cnt = team_pairing_count.setdefault(pt, 0) + 1
                team_pairing_count[pt] = cnt

    def _further_from_others(self, options, clashes):
        opt_distances = []
        for opt_idx in options:
            opt_distances.append(
                (sum([brackets_depth_distance(clashes, opt_idx, cl_idx)
                      for cl_idx in range(len(clashes))
                      if clashes[cl_idx].competitor_a]
                     ),
                 opt_idx)
            )
        opt_distances.sort()
        return opt_distances[-1][1]

    def _assign_by_rating(self, clashes, competitor_list, reservations,
                          team_pairing_count):
        """Sort competitors by rating, and tries to put them as separate
        as possible, respecting team.
        """
        # TODO: do we really need team count for something ?:
        team_count = clashes_team_count(reservations)
        sorted_clist = sorted(
                competitor_list,
                key=lambda comp: (-comp.rating, -team_count[comp.team]))
        for idx, comp in enumerate(sorted_clist):
            options = self._find_competitor_clash_options(
                    comp, reservations, clashes)
            if not options:
                # TODO: Should we raise an exception?
                continue
            f_idx = self._further_from_others(options, clashes)
            self._assign_clash(comp, clashes, f_idx,
                               reservations, team_pairing_count)
            # This would be the naive option of alternating :
            # cheaper in computing costs but less fair
            #self._assign_clash(comp, clashes, options[-(idx % 2)],
            #                   reservations, team_pairing_count)

    def _assign_by_random(self, clashes, competitor_list, reservations,
                          team_pairing_count):
        for idx, comp in enumerate(competitor_list):
            options = self._find_competitor_clash_options(
                    comp, reservations, clashes)
            self._assign_clash(comp, clashes, self.rnd.choice(options),
                               reservations, team_pairing_count)

    def generate(self, competitor_list, team_pairing_count=None):
        if team_pairing_count is None:
            team_paiting_count = dict()
        assign_single_competitor_teams = not self.use_rating
        if self.use_teams:
            clashes, reservations = create_reserved_teams_bracket_clashes(
                competitor_list, team_pairing_count, rnd=self.rnd,
                assign_single_competitor_teams=assign_single_competitor_teams)
        else:
            clashes = generate_first_round_clashes(len(competitor_list))
            reservations = [list() for _ in clashes]
        if not clashes:
            return clashes
        if self.use_rating:
            self._assign_by_rating(clashes, competitor_list, reservations,
                                   team_pairing_count)
        else:
            self._assign_by_random(clashes, competitor_list, reservations,
                                   team_pairing_count)
        return clashes


#!/usr/bin/env python3
from aws_cdk import core
from stack.droplet import DropletStack

envDev = core.Environment( region = "us-east-1" )

app = core.App()

stack = DropletStack( app, "relic-dev" , env = envDev )

website = stack.addSite( "Website", "relic-www-bucket-dev", "html/build/" )

userPool = stack.addUserPool( "userPool", "relic-users" )

table = stack.addTable( "Characters", "relic-chars", "id" )

api = stack.addApi( "NewApi" )

app.synth()


class Solution(object):
    def findSingle(self, nums):
        result = set()
        repeated = set()
        for x in nums:
            if x in result:
                result.discard(x)
                repeated.add(x)
            elif x not in repeated:
                result.add(x)
        return result.pop()

nums = [1, 1, 3, 4, 4, 5, 6, 5, 6]
print(Solution().findSingle(nums))
# 3