#!/usr/bin/python3.5
# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd
import parselmouth
from scipy.interpolate import lagrange, interp1d
from scipy import stats

def get_pitch(filepath, **kwargs):
    df_pitch = None
    snd = None
    time_step = 0.01,
    pitch_floor = 75.0,
    max_number_of_candidates = 15,
    very_accurate = True,
    silence_threshold = 0.03,
    voicing_threshold = 0.45,
    octave_cost = 0.01,
    octave_jump_cost = 0.35,
    voiced_unvoiced_cost = 0.14,
    ceiling = 600.0
    for key, value in kwargs.items():
        if key is "time_step": time_step = value
        if key is "pitch_floor": pitch_floor = value
        if key is "max_number_of_candidates": max_number_of_candidates = value
        if key is "very_accurate": very_accurate = value
        if key is "silence_threshold": silence_threshold = value
        if key is "voicing_threshold": voicing_threshold = value
        if key is "octave_cost": octave_cost = value
        if key is "octave_jump_cost": octave_jump_cost = value
        if key is "voiced_unvoiced_cost": voiced_unvoiced_cost = value
        if key is "ceiling": ceiling = value
    if ".txt" in filepath:
        df = pd.read_csv(filepath, sep="\s+")
        df_pitch = df.replace("--undefined--", 0).astype("float64")
        # snd = ""
    if ".wav" in filepath:
        snd = parselmouth.Sound(filepath)
        pitch = snd.to_pitch_ac(time_step=time_step,
                                pitch_floor=pitch_floor,
                                max_number_of_candidates=max_number_of_candidates,
                                very_accurate=very_accurate,
                                silence_threshold=silence_threshold,
                                voicing_threshold=voicing_threshold,
                                octave_cost=octave_cost,
                                octave_jump_cost=octave_jump_cost,
                                voiced_unvoiced_cost=voiced_unvoiced_cost,
                                ceiling=ceiling
                                )
        pitch_values = np.array(pitch.selected_array['frequency'])
        pitch_xs = np.round(np.array(pitch.xs()), 4)
        pitch_1 = np.c_[pitch_xs, pitch_values]
        df_pitch = pd.DataFrame(pitch_1, columns=["Time_s", "F0_Hz"])

    return df_pitch, snd

def compare(value, df_vad):
    result = "OFF"
    for row in df_vad.itertuples():
        if (value >= row.ON) and (value <= row.OFF):
            # print(row.Index)
            result = "ON"
            index = row.Index
    return result

def get_vad_index(df_pitch, time_step=0.1, off_threshold=0.5, on_threshold=0.5):
    count = 0
    # print(threshold)
    buff = False
    for row in df_pitch.itertuples():
        if row.VAD == "ON":
            #온이 하나라도 있으면
            check_on_length = df_pitch.loc[row.Index - int(off_threshold / time_step):row.Index - 1, "VAD"].isin(["ON"]).tolist()
            #오프가 하나라도 있으면
            check_off_lenth = df_pitch.loc[row.Index: row.Index + int(on_threshold / time_step) - 1, "VAD"].isin(["OFF"]).tolist()
            if buff is False and True not in check_on_length and True not in check_off_lenth:
                count += 1
                buff = True
            df_pitch.loc[row.Index, "VAD_INDEX"] = count
        else:
            df_pitch.loc[row.Index, "VAD_INDEX"] = 0
            buff = False
    return df_pitch

def add_vad(df_pitch, df_vad=None, time_step=0.1, off_threshold=0.5, on_threshold=0.5, **kwargs):
    df_return = None
    for key, value in kwargs.items():
        if key is "source" and value is "praat":
            df_pitch["VAD"] = np.where(df_pitch["F0_Hz"] != 0, "ON", "OFF")
            df_return = get_vad_index(df_pitch, time_step=time_step, off_threshold=off_threshold, on_threshold=on_threshold)
        if key is "source" and value is "manual":
            df_pitch["VAD"] = df_pitch["Time_s"].map(lambda x: compare(x, df_vad))
            df_return = get_vad_index(df_pitch, time_step=time_step, off_threshold=off_threshold, on_threshold=on_threshold)

    return df_return

def make_delta_pitch(df):
    df = df.reset_index(drop=True)
    remove_idx = []
    df["DELTA_PITCH"] = df["RAW_PITCH"] - df['RAW_PITCH'].shift(1)
    for row in df.itertuples():
        if (row.Index > 0) and (df.loc[row.Index - 1, "VAD"] == "OFF") and (df.loc[row.Index, "VAD"] == "ON"):
            remove_idx.append(row.Index)
    df_return = df.drop(df.index[remove_idx])
    df_return = df_return.reset_index(drop=True)

    return df_return

def make_smooth_pitch(df, window=5, reject_vad=True):
    # 한 서브젝트의 Active Speech 당 지난 5개의 샘플을 에 대한 평균값

    if reject_vad is True:
        df = df[(df['VAD'] != "OFF")]
    # df = df.copy()
    df_return = pd.DataFrame(dtype=float)

    subject_list = get_index_list(df, collection="SUBJECT_NUM")
    for subject in subject_list:
        df_subject = df[(df['SUBJECT_NUM'] == subject)].copy()
        episode_list = get_index_list(df_subject, collection="EPISODE_NUM")
        for episode in episode_list:
            df_episode = df_subject[(df_subject['EPISODE_NUM'] == episode)].copy()
            vad_index_list = get_index_list(df_episode, collection="VAD_INDEX")
            for vad_index in vad_index_list:
                df_vad_index = df_episode[(df_episode['VAD_INDEX'] == vad_index)].copy()
                df_vad_index["SMOOTH_PITCH"] = df_vad_index["RAW_PITCH"].rolling(window=window, min_periods=1).mean()
                df_return = df_return.append(df_vad_index, ignore_index=True)

    df_return = df_return.reset_index(drop=True)

    return df_return

def get_index_list(df, collection="SUBJECT_NUM"):
    df_return = df[collection].drop_duplicates().values
    return df_return

def make_interpolation_function(df, func="linear", reject_vad=True):
    # interpolation option
    # 보간법을 썼을때는 resampling을 해야하는데....
    # 어떤식으로 의미가 있을가?
    # 일단 패스
    # 각 Speech Activityㄷ 당 해야함
    if reject_vad is True:
        df = df[(df['VAD'] != "OFF")]

    xs = df["Time_s"].values
    ys = df["RAW_PITCH"].values

    if func is "linear":
        f = interp1d(xs, ys, kind="linear")

    elif func is "quadratic":
        f = interp1d(xs, ys, kind="quadratic")

    elif func is "cubic":
        f = interp1d(xs, ys, kind="cubic")

    elif func is "lagrange":
        f = lagrange(xs, ys)

    return xs, ys, f

from fa_tools import df_handler

def make_vad_feature(df, reject_vad=True, collection="RAW_PITCH"):
    if reject_vad is True:
        df = df[(df['VAD'] != "OFF")]

    df_return = pd.DataFrame(dtype=float)
    df_return_plot = pd.DataFrame(dtype=float)

    subject_list = get_index_list(df, collection="SUBJECT_NUM")
    for subject in subject_list:
        df_subject = df[(df['SUBJECT_NUM'] == subject)].copy()
        episode_list = get_index_list(df_subject, collection="EPISODE_NUM")
        for episode in episode_list:
            df_episode = df_subject[(df_subject['EPISODE_NUM'] == episode)].copy()
            vad_index_list = get_index_list(df_episode, collection="VAD_INDEX")
            for vad_index in vad_index_list:
                df_vad_index = df_episode[(df_episode['VAD_INDEX'] == vad_index)].copy()

                ys = df_vad_index[collection]
                mean = ys.mean()
                median = ys.median()
                minimum = ys.min()
                maximum = ys.max()
                std = ys.std()
                trim_mean = stats.trim_mean(ys, 0.3)

                # print(df_vad_index["E"].iloc[0])
                # print(df_vad_index["N"].iloc[0])
                E = df_vad_index["E"].iloc[0]
                N = df_vad_index["N"].iloc[0]

                df_vad_index["MEAN"] = mean
                df_vad_index["MEDIAN"] = median
                df_vad_index["MINIMUM"] = minimum
                df_vad_index["MAXIMUM"] = maximum
                df_vad_index["STD"] = std
                df_vad_index["TRIM_MEAN"] = trim_mean

                s = pd.Series([subject, episode, vad_index, mean, median, minimum, maximum, std, trim_mean, E, N],
                              index=['SUBJECT_NUM', 'EPISODE_NUM', 'VAD_INDEX', "MEAN", "MEDIAN", "MINIMUM", "MAXIMUM", "STD", "TRIM_MEAN", "E", "N"])
                df_return = df_return.append(s, ignore_index=True)

                df_return_plot = df_return_plot.append(df_vad_index, ignore_index=True)

    df_return = df_return.reset_index(drop=True)
    df_return_plot = df_return_plot.reset_index(drop=True)

    return df_return, df_return_plot
    # print(ys)
    # print(trim_mean)

# y_lin = f_lin(xs)
# y_quad = f_quad(xs)
# y_cubic = f_cubic(xs)
# y_lagrange = f_lagrange(xs)

def make_baseline(df, feature="intensity"):
    df = df[(df['VAD'] == "OFF")]
    print(df)


# itertools is a standard library that contains several functions that are useful in functional programming

#Example: The function count() counts up infinitely from a value
#		  The function cycle() infinitely iterates through an iterable(for instance : a list or a string)
#		  The function repeat() repeats an object , either infinitely or a specified number of times
#		  The function takewhile() takes items from an iterable while a predicate function remains true
#		  The fucntion chain() combines several iterables into one long one
#		  The function accumulate() returns a running total of the values in an iterable

from itertools import count , repeat , cycle , takewhile , chain , accumulate

import time

for i in count(5):
	if i == 11:
		break
	print(i)

t0 = time.clock()				#cpu-time before this function starts
for i in cycle("hello"):
	print(i,end=" ")
	t1 = time.clock()			#cpu-time now
	if t1 - t0 > 2:
		break					#this function runs for 2 seconds

print(list(repeat("Good things",5)))

nums = list(accumulate(range(8)))
print(nums)						#[0, 1, 3, 6, 10, 15, 21, 28]

print(list(takewhile(lambda x:x<=6 , nums)))	# [0, 1, 3, 6]

odds = [1,3,5,7,9]
evens = [2,4,6,8]

nums = list(chain(odds,evens))
print(nums)						# [1, 3, 5, 7, 9, 2, 4, 6, 8]


# -*- coding: utf-8 -*-

"""
direct Python Toolbox
All-in-one toolbox to encapsulate Python runtime variants
----------------------------------------------------------------------------
(C) direct Netware Group - All rights reserved
https://www.direct-netware.de/redirect?dpt;interactive_cli

This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------------------------
https://www.direct-netware.de/redirect?licenses;mpl2
----------------------------------------------------------------------------
unittest
"""

import unittest

from dpt_interactive_cli import InteractiveCli

class TestInteractiveCli(unittest.TestCase):
    """
Unittest for InteractiveCli

:since: v1.0.0
    """

    def test_html_output(self):
        """
Tests HTML special characters.
        """

        interactive_cli = InteractiveCli()
        interactive_cli.output_info("<this> is HTML")
        interactive_cli.output_error("<this> is HTML as well")
        interactive_cli.output_info("<{0}> is still HTML", "this")
        interactive_cli.output_error("<{0}> is still HTML as well", "this")
    #
#

if (__name__ == "__main__"):
    unittest.main()
#


ans_1 = []
ans_2 = []
temp1 = [0,1,0,1,0,1,0,1]
temp2 = [1,0,1,0,1,0,1,0]

def diff(arr1, arr2):
    sum = 0
    for i in range(8):
        for j in range(8):
            if arr1[i][j]!= arr2[i][j]:
                sum += 1
    return sum

for i in range(8):
    if i%2 == 0:
        ans_1.append(temp1)
        ans_2.append(temp2)
    else:
        ans_1.append(temp2)
        ans_2.append(temp1)

n,m = map(int,input().split())
ori = []
for i in range(n):
    temp = input()
    t = []
    for j in temp:
        if j == "B":
            t.append(0)
        else:
            t.append(1)
    ori.append(t)

min = 64

for i in range(n-8+1):
    for j in range(m-8+1):
        a = diff([t[j:j+8] for t in ori[i:i+8]], ans_1)
        b = diff([t[j:j+8] for t in ori[i:i+8]], ans_2)
        if a<= min:
            min = a
        if b<= min:
            min = b
print(min)


'''
Write a Python program that computes the net amount of a bank account based a
transaction log from console input. The transaction log format is shown as following: D
100 W 200 (Withdrawal is not allowed if balance is going negative. Write functions for
withdraw and deposit) D means deposit while W means withdrawal.
Suppose the following input is supplied to the program:
D 300
D 300
W 200
D 100
Then, the output should be: 500
'''

if __name__=="__main__":

    condition=True
    balance=0

    transactionList=[]
    while(condition):
        transaction=input("\nEnter the transaction log: ")
        if(transaction=='x'):
            condition=False
            print("\nThank You for using this program!!\n")
        else:
            transactionSplit=transaction.split()
            if(transactionSplit[0]=='D' or transactionSplit[0]=='W' ):
                if(transactionSplit[0]=='D'):
                    balance=balance+int(transactionSplit[1])
                    transactionList.append(transactionSplit)
                    print("\nTransaction List : ",transactionList)
                    print("\nBalance: ",balance)
                elif( transactionSplit[0]=='W' ):
                    if(balance>=int(transactionSplit[1])):
                        balance=balance-int(transactionSplit[1])
                        transactionList.append(transactionSplit)
                        print("\nTransaction List : ",transactionList)
                        print("\nBalance: ",balance)
                    else:
                        print("\nWithdraw not possible because balance is less!!")
                        print("\nTransaction List : ",transactionList)
                        print("\nBalance: ",balance)
            else:
                print("\nPlease Enter the Valid Log!!!")


import pandas as pd
import networkx as nx
'''['state', 'submitTime', 'startTime', 'finishTime', 'queueTime',
       'runTime', 'NumMaps', 'avgMapTime', 'avgReduceTime', 'avgShuffleTime',
       'avgMergeTime', 'NumReduce', 'HDFS_INPUT_SIZE', 'HDFS_OUTPUT_SIZE',
       'MAP_CPU_USAGE_MSEC', 'REDUCE_CPU_USAGE_MSEC', 'MAP_MEM_USAGE_B',
       'REDUCE_MEM_USAGE_B', 'HIVE_RECORDS_IN', 'HIVE_RECORDS_OUT',
       'HIVE_RECORDS_INTERMEDIATE', 'SLOTS_MILLIS_MAPS',
       'SLOTS_MILLIS_REDUCES', 'TOTAL_LAUNCHED_MAPS', 'TOTAL_LAUNCHED_REDUCES',
       'DATA_LOCAL_MAPS', 'RACK_LOCAL_MAPS', 'MILLIS_MAPS', 'MILLIS_REDUCES',
       'VCORES_MILLIS_MAPS', 'VCORES_MILLIS_REDUCES', 'MB_MILLIS_MAPS',
       'MB_MILLIS_REDUCES', 'PHMAP_MEM_USAGE_B', 'PHREDUCE_MEM_USAGE_B',
       'PHPHYSICAL_MEMORY_B', 'jobid', 'job.maps', 'query', 'outputdir',
       'scratchdir', 'sessionid', 'query.id', 'local.scratchdir', 'tmpouput',
       'user.name', 'job', 'n_inputs', 'inputdir', 'workflow.node',
       'workflow.id', 'workflow.dag', 'table.name', 'submit_ts',
       'submit_ts_p10m']
'''

def getDag(df):
  print(getDag.__name__)
  df_dep = df[['startTime','inputdir','outputdir','workflowid']]
  dags = {}
  workflow_list = df_dep.workflowid.unique()
  for key in workflow_list:
    workflow = df_dep.loc[(df['workflowid'] == key)]
    workflow = workflow.sort_values('startTime')
    workflow = workflow.drop(['startTime', 'workflowid'], axis=1)
    edges = workflow.values.tolist()
    graph = nx.DiGraph()
    graph.add_edges_from(edges)
    dags[key]=graph
  for k in dags.keys():
    print(k,'->',list(nx.topological_sort(dags[k])))

def traceParser(filename):
#  df = pd.read_csv(filename, sep=',',skipinitialspace=True, header = None, nrows=5, usecols=[2,3,12,13,39,45,48,50])
  df = pd.read_csv(filename, sep=',',skipinitialspace=True, header = None,  usecols=[0,1,2,3,4,5,6])
 # df.columns = ['startTime','finishTime','mapper','input_size','output_size','outputdir','user_name','inputdir','workflowid','iotype']

  df.columns = ['startTime','mapper','input_size','inputdir','user_name','workflowid','iotype']
  df.loc[df['iotype'] == 'delete', ['mapper']] = 1
  # round input to multiple of 4

  base = 4
  df['mapper_input_size'] = base * round( (df.input_size / df.mapper)/base)
  df.mapper_input_size = df.mapper_input_size.astype(int)
  df.input_size = df.input_size.astype(int)
  print(df)
  return df

#def createFinalTrace(df):
  df_r = df[['startTime','user_name','inputdir', 'input_size','workflowid']]
  df_w = df[['finishTime','user_name','outputdir', 'output_size','workflowid']]
  del df
  df_r = df_r.rename(columns={'inputdir':'objname', 'input_size':'size'})
  df_w = df_w.rename(columns={'finishTime':'startTime','outputdir':'objname', 'output_size':'size'})
  df_r['io'] = 'read'
  df_w['io'] = 'write'
  frames = [df_r, df_w]
  df = pd.concat(frames)

  # Create Deletion for intermediate datasets
  workflow_list = df.workflowid.unique()

  G = nx.DiGraph()
  G.add_edge(1,2)
  dags = {}
  for key in workflow_list:
    workflow1 = df.loc[(df['workflowid'] == key)]
    workflow1 = workflow1.sort_values('startTime')
    print(workflow1)
    workflow = df.loc[(df['workflowid'] == key) & (df['io'] == 'write')]
    workflow = workflow.sort_values('startTime')
    query_completion_time = workflow.startTime.tail(1) + 1

    workflow = workflow.iloc[:-1]
#    print(workflow)
    write_list = workflow.loc[workflow['io'] == 'write']
    print("----")
    inputs = workflow1.objname.values.tolist()
    print(inputs)

  #df = df.sort_values('startTime')

  #print(df)
  del df_r, df_w
  #df['startTime']= pd.to_datetime(df['startTime'])


import os

rawdata = list()
with open(os.path.join(os.path.dirname(__file__), 'input.txt'), "r", encoding="utf8") as f:
    for ligne in f:
        rawdata.append(ligne.replace('\n',""))

frequence = 0
valeurs = list()
index = 0

while True:
    if frequence in valeurs:
        print(frequence)
        break
    else:
        valeurs.append(frequence)
    signe = rawdata[index][0]
    valeur = int(rawdata[index][1:])
    if signe == "+":
        frequence += valeur
    elif signe == "-":
        frequence -= valeur
    index += 1
    if index == len(rawdata):
        index = 0


from comet_ml import Experiment  # isort:skip

import tensorflow as tf

# Assumes Comet variable environment configuration.
# See here to get your API_KEY:
# https://www.comet.ml/user/settings/account#section-DEVELOPER_INFORMATION
# And here for setup information:
# https://www.comet.ml/docs/python-sdk/advanced/#experiment-configuration-parameters
experiment = Experiment()

mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential(
    [
        tf.keras.layers.Flatten(input_shape=(28, 28)),
        tf.keras.layers.Dense(128, activation="relu"),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(10),
    ]
)

loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

model.compile(optimizer="adam", loss=loss_fn, metrics=["accuracy"])

model.fit(x_train, y_train, epochs=10)


from requests import get
from json import loads
from terminaltables import AsciiTable

CITIES_LIST = ['Katowice', 'Kraków', 'Rzeszów', 'Warszawa']

def main():
    url = 'https://danepubliczne.imgw.pl/api/data/synop'
    response = get(url)
    rows = [['Miasto', 'Godzina pomiaru', 'Temperatura', 'Ciśnienie']]

    for row in loads(response.text):
        if row['stacja'] in CITIES_LIST:
            rows.append([row['stacja'], row['godzina_pomiaru'], row['temperatura'], row['cisnienie']])
    table = AsciiTable(rows)
    print(table.table)

if __name__ == '__main__':
    print("Weather checker")
    main()


def exo9(n):
	for i in range(1,n):
		for j in range(i+1,n):
			k=n-i-j
			if i**2+j**2==k**2:
				p=i*j*k

	return p

assert(exo9(12)==60)
print(exo9(1000))


from revabapp.src.revab import check_user_guess, load_words, NO_REVABS_POSSIBLE
import re
import json

EMPTY = "..."
NO_REVABS_GUESS_MESSAGE = "No revabs exist"

def validate_input(value, pattern):
    """
    Return True if value is not None, a str, and if it matches the pattern
    pattern: raw string that's a regular expression
    """
    if value is None:
        return False

    if type(value) != str:
        return False

    if not re.match(pattern, value):
        return False

    return True

def validate_rounds(rounds):
    """
    Return True if rounds is a one or two digit number, False otherwise
    """
    rounds_pattern = r'^[123456789][0123456789]?$'
    return validate_input(rounds, rounds_pattern)

def validate_attempts_per_round(attempts_per_round):
    """
    Return True if attempts_per_round is a number from 1 to 5, False otherwise
    """
    attempts_per_round_pattern = r'^[12345]$'
    return validate_input(attempts_per_round, attempts_per_round_pattern)

def validate_abbrev_length(abbrev_length):
    """
    Return True if abbrev_length is 3 or 4, False otherwise
    """
    abbrev_length_pattern = r'^[34]$'
    return validate_input(abbrev_length, abbrev_length_pattern)

def validate_abbrev(abbrev):
    """
    Return True if abbrev consists of 3 or 4 letters, False otherwise
    """
    abbrev_pattern = r'^[a-zA-Z]{3,4}$'
    return validate_input(abbrev, abbrev_pattern)

def validate_guess(guess):
    """
    Return True if guess is at most 25 characters, False otherwise
    """
    guess_pattern = r'^[a-zA-Z]{1,25}$'
    return validate_input(guess, guess_pattern)

def validate_total_points(total_points):
    """
    Return True if total_points is a 1 to 3 digit number, False otherwise
    """
    total_points_pattern = r'^\d{1,3}$'
    return validate_input(total_points, total_points_pattern)

def validate_round_history(round_history, rounds=None):
    """
    Return True if round_history is in valid format, False otherwise
    """
    if round_history is None:
        return False

    if type(round_history) != str:
        return False

    try:
        round_history = json.loads(round_history.replace("\'", "\""))
    except json.JSONDecodeError:
        return False

    if type(round_history) != list:
        return False

    #length of round history should be the number of rounds
    if len(round_history) != rounds and rounds is not None:
        return False

    #each element in round history should be a dict with the keys 'number', 'abbrev', 'best_guess', and 'score'
    for round in round_history:
        if type(round) != dict:
            return False

        if set(round.keys()) != {"number", "abbrev", "best_guess", "score"}:
            return False

    #each number should be one more than the index of its round
    for index, round in enumerate(round_history):
        if round["number"] != index + 1:
            return False

    #either all or none of 'abbrev', 'best_guess', and 'score' should be '...'
    #if one round is all '...', then all of the following rounds should be '...' as well
    is_future_round = False
    for round in round_history:
        round_info = {round["abbrev"], round["best_guess"], round["score"]}

        #some fields are filled, some fields are empty. This shouldn't happen
        if EMPTY in round_info and len(round_info) > 1:
            return False

        #this is a round that comes after an empty one but it is non-empty. This shouldn't happen
        if is_future_round and EMPTY not in round_info:
            return False

        #any round that comes after this one should have all empty fields
        if EMPTY in round_info:
            is_future_round = True

    #all 'abbrev's that aren't '...' should be the same length, either 3 or 4 letters (alphabetical)
    abbrev_pattern = r'^[a-zA-Z]{3,4}$'
    for round in round_history:
        abbrev = round["abbrev"]
        if abbrev == EMPTY:
            continue

        if type(abbrev) != str:
            return False

        if not re.match(abbrev_pattern, abbrev):
            return False

    #all 'best_guess' should be strings (but no restrictions on their content)
    for round in round_history:
        if type(round["best_guess"]) != str:
            return False

    words = load_words()
    #all scores should be ints, and the points from best_guess on the abbrev should match the score listed
    for round in round_history:
        score = round["score"]
        if score == EMPTY:
            continue

        if type(score) != int:
            return False

        #there is a special string I use to submit user guess when they say that no revabs exist,
        # and I don't display that string in the round history (when it is the best guess that round).
        # Instead of submitting the displayed guess, I submit the special "no revabs" string.
        guess = round["best_guess"]
        if round["best_guess"] == NO_REVABS_GUESS_MESSAGE:
            guess = NO_REVABS_POSSIBLE
        _, correct_score = check_user_guess(round["abbrev"], guess, words)
        if score != correct_score:
            return False

    return True

def validate_guess_history(guess_history, attempts_per_round, abbrev):
    """
    Return True if guess_history is in a valid format, False otherwise
    """
    if guess_history is None:
        return False

    if type(guess_history) != str:
        return False

    try:
        guess_history = json.loads(guess_history.replace("\'", "\""))
    except json.JSONDecodeError:
        return False

    if type(guess_history) != list:
        return False

    #length of guess history should be the number of allowed guessed per round
    if len(guess_history) != attempts_per_round:
        return False

    #each element in guess history should be a dict with the keys 'number', 'guess', 'result', and 'score'
    for guess in guess_history:
        if type(guess) != dict:
            return False

        if set(guess.keys()) != {"number", "guess", "result", "score"}:
            return False

    #each number should be one more than the index of its guess
    for index, guess in enumerate(guess_history):
        if guess["number"] != index + 1:
            return False

    #either all or none of 'guess', 'result', and 'score' should be '...'
    #if one guess is all '...', then all of the following guesses should be '...' as well
    is_future_guess = False
    for guess in guess_history:
        guess_info = {guess["guess"], guess["result"], guess["score"]}

        #some fields are filled, some fields are empty. This shouldn't happen
        if EMPTY in guess_info and len(guess_info) > 1:
            return False

        #this is a guess that comes after an empty one but it is non-empty. This shouldn't happen
        if is_future_guess and EMPTY not in guess_info:
            return False

        #any guess that comes after this one should have all empty fields
        if EMPTY in guess_info:
            is_future_guess = True

    #all guesses should be strings
    for guess in guess_history:
        if type(guess["guess"]) != str:
            return False

    #result and score should match what we get from checking the user's guess against the abbrev
    words = load_words()
    for guess in guess_history:
        user_guess = guess["guess"]
        if user_guess == EMPTY:
            continue

        if user_guess == NO_REVABS_GUESS_MESSAGE:
            user_guess = NO_REVABS_POSSIBLE
        outcome, score = check_user_guess(abbrev, user_guess, words)
        if guess["result"] != outcome.value:
            return False
        if guess["score"] != score:
            return False

    return True


import logging
import minio
import NRUtil.NRObjStoreUtil
import admin.snow_path_lib
import os.path
import admin.constants

LOGGER = logging.getLogger(__name__)

class OStore:
    def __init__(self):
        self.ostore = NRUtil.NRObjStoreUtil.ObjectStoreUtil()
        self.historical_norms_path = "norm/{sat}/daily/{period}/{month}.{day}.tif"
        self.snow_path = admin.snow_path_lib.SnowPathLib()
        self.cache = {}

    def get_10yr_tif(self, sat, month, day, out_path):
        if not os.path.exists(out_path):
            ostore_path = self.historical_norms_path.format(
                period="10yr", month=month, day=day, sat=sat
            )
            LOGGER.debug(f"pulling the 10yr file: {ostore_path} down from obj store")
            self.ostore.get_object(file_path=ostore_path, local_path=out_path)

    def get_20yr_tif(self, sat, month, day, out_path):
        if not os.path.exists(out_path):

            ostore_path = self.historical_norms_path.format(
                period="20yr", month=month, day=day, sat=sat
            )
            LOGGER.debug(f"pulling the 20yr file: {ostore_path} down from obj store")
            self.ostore.get_object(file_path=ostore_path, local_path=out_path)

    def get_mosaic_plot_dir(self, date: str, sat: str):
        ostore_util = NRUtil.NRObjStoreUtil.ObjectStoragePathLib()

        if sat.lower() == 'modis':
            plot_dir = admin.constants.PLOT_MODIS_MOSAIC
        elif sat.lower() == 'viirs':
            plot_dir = admin.constants.PLOT_VIIRS_MOSAIC
        else:
            msg = f'unknown sat type: {sat}'
            raise ValueError(msg)
        # ./data/plot/modis/mosaic/2023.03.21/2023.03.21.png
        ostore_util = NRUtil.NRObjStoreUtil.ObjectStoragePathLib()
        os_plot_dir = ostore_util.remove_sr_root_dir(plot_dir, admin.constants.TOP)
        expected_ostore_path = os.path.join(
            admin.constants.OBJ_STORE_TOP, os_plot_dir, date)
        return expected_ostore_path

    def mosaic_plot_exists(self, date: str, sat: str):
        """returns a boolean if the expected plot exists in object storage or not

        :param date: date string in the format YYYY.MM.DD
        :type date: str
        :param sat: satellite type, allowed values (modis | viirs)
        :type sat: str
        :raises ValueError: _description_
        """
        file_exists = False
        plot_file_name = f'{date}.png'
        expected_ostore_path = self.get_mosaic_plot_dir(date=date, sat=sat)

        # ./data/plot/modis/mosaic/2023.03.21/2023.03.21.png
        files_in_ostore = self.ostore.list_objects(
            objstore_dir=expected_ostore_path,
            recursive=True,
            return_file_names_only=True)
        just_file_names = [os.path.basename(plot_file) for plot_file in files_in_ostore]
        if plot_file_name in just_file_names:
            file_exists = True
        return file_exists

    def push_daily_mosaic_plot(self, date, sat, local_path):
        """_summary_

        :param date: _description_
        :type date: _type_
        :param sat: _description_
        :type sat: _type_
        :param local_path: _description_
        :type local_path: _type_
        """
        if not os.path.exists(local_path):
            msg = f'Trying to push the file: {local_path} but it doesn\'t exist'
            ValueError(msg)
        plot_file_name = f'{date}.png'
        plot_dir_name = self.get_mosaic_plot_dir(date=date, sat=sat)
        ostore_full_path = os.path.join(plot_dir_name, plot_file_name)

        self.ostore.put_object(
            ostore_path=ostore_full_path,
            local_path=local_path
        )

    def process_modis_data_exists(self, date, dates):
        """identifies if the data that is going to be used to generate modis
        plots exists in object storage.

        :param date: _description_
        :type date: _type_
        :param dates: _description_
        :type dates: _type_
        """
        # self.snow_path.
        # self.get_mod
        pass

    def get_ostore_path(self, local_path):
        ostr_lib = NRUtil.NRObjStoreUtil.ObjectStoragePathLib()
        ostore_path = ostr_lib.get_obj_store_path(
            src_path=local_path,
            ostore_path=admin.constants.OBJ_STORE_TOP,
            src_root_dir=admin.constants.TOP,
            prepend_bucket=False
        )
        return ostore_path

    def get_file(self, local_path):
        ostore_path = self.get_ostore_path(local_path)
        self.ostore.get_object(
            file_path=ostore_path,
            local_path=local_path
        )

    def ostore_file_exists(self, local_path):
        exists = False
        orig_dir = os.path.dirname(local_path)
        ostore_path = self.get_ostore_path(local_path=local_path)
        if orig_dir not in self.cache:
            ostr_dir = os.path.dirname(ostore_path)
            ostr_files = self.ostore.list_objects(
                objstore_dir=ostr_dir, recursive=True, return_file_names_only=True
            )
            self.cache[ostr_dir] = ostr_files
        if ostore_path in self.cache[ostr_dir]:
            # file is in ostore so pull it
            exists = True
        return exists

    def get_file_if_exists(self, local_file):
        LOGGER.debug(f"local file: {local_file}")
        if not os.path.exists(local_file):
            if self.ostore_file_exists(local_path=local_file):
                # file is in ostore so pull it
                LOGGER.debug(f"pulling {local_file} from ostore")
                self.get_file(local_file)