import numpy as np
import bs4 as bs
import matplotlib.pyplot as plt
from matplotlib import style
import datetime as dt
import pandas as pd
import pandas_datareader.data as web
import os
import pickle
import requests
import seaborn as sns
from sklearn import svm, model_selection, neighbors

style.use('ggplot')

# Extracts the tickers for the s&p 500 from wikipedia table and places them into a list.
def save_sp500_tickers():
    resp = requests.get('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
    soup = bs.BeautifulSoup(resp.text, 'lxml')
    table = soup.find('table', {'id': 'constituents'})
    tickers = []
    for row in table.findAll('tr')[1:]:
        ticker = row.find('td').text.strip()
        if "." in ticker:
            ticker = ticker.replace('.', '-')
            print('ticker replaced to', ticker)
        tickers.append(ticker)

# Pickle is used to convert the python object hierarchy into a byte stream.
    with open('sp500_tickers.pickle', 'wb') as f:
        pickle.dump(tickers, f)

    print(tickers)

    return tickers

# Individual functions only for testing purposes, they are incorporated into final function
# save_sp500_tickers()

# Function to iterate through list of tickers and extract company data from yahoo.
# Puts each company data into a csv in a folder called stock_dfs.
# Iterating through all companies to get pricing data takes time (>20 mins) so save locally.
def get_data_from_yahoo(reload_sp500=False):
    if reload_sp500:
        tickers = save_sp500_tickers()
    else:
        with open('sp500_tickers.pickle', 'rb') as f:
            tickers = pickle.load(f)

    if not os.path.exists('stock_dfs'):
        os.makedirs('stock_dfs')

    start = dt.datetime(2000, 1, 1)
    end = dt.datetime(2020, 8, 31)

    for ticker in tickers:
        if not os.path.exists('stock_dfs/{}.csv'.format(ticker)):
            print(ticker)
            df = web.get_data_yahoo(ticker, start, end)
            df.to_csv('stock_dfs/{}.csv'.format(ticker))
        else:
            print('Already have {}'.format(ticker))

# Individual functions only for testing purposes, they are incorporated into final function
# get_data_from_yahoo()

# Compile adjusted close data for all companies into a pandas dataframe and export as a csv
def compile_data():
    with open("sp500_tickers.pickle", 'rb') as f:
        tickers = pickle.load(f)

    main_df = pd.DataFrame()

    for count, ticker in enumerate(tickers):
        df = pd.read_csv('stock_dfs/{}.csv'.format(ticker))
        df.set_index('Date', inplace=True)

        df.rename(columns={'Adj Close': ticker}, inplace=True)
        df.drop(['Open', 'High', 'Low', 'Close', 'Volume'], 1, inplace=True)

        if main_df.empty:
            main_df = df
        else:
            main_df = main_df.join(df, how='outer')

        if count % 10 == 0:
            print(count)

    print(main_df.head())
    main_df.to_csv('sp500_joined_closes.csv')

# Individual functions only for testing purposes, they are incorporated into final function
# compile_data()

# The combined csv file is loaded into pandas and df.corr used to create a correlation table.
# Pct change is used to convert from stock prices to stock returns as it os more useful for finance.
# Correlation plot is generated using seaborn.
def visualize_data():
    df = pd.read_csv('sp500_joined_closes.csv')
    df.set_index('Date', inplace=True)
    df_corr = df.pct_change().corr()
    print(df_corr.tail())
    sns.heatmap(df_corr, annot=False, cmap='RdYlGn')
    plt.show()

# visualize_data()

# Each model will be on a per company basis
def process_data_for_labels(ticker):
    how_many_days = 7
    df = pd.read_csv('sp500_joined_closes.csv', index_col=0)
    tickers = df.columns.values.tolist()
    df.fillna(0, inplace=True)

    for i in range(1, how_many_days + 1):
        df['{}_{}d'.format(ticker, i)] = (df[ticker].shift(-i) - df[ticker]) / df[ticker]

    df.fillna(0, inplace=True)
    return tickers, df

# Individual functions only for testing purposes, they are incorporated into final function.
# process_data_for_labels('XOM')

# Function to help create labels, may need to alter requirement value
def buy_sell_hold(*args):
    cols = [c for c in args]
    requirement = 0.02
    for col in cols:
        if col > requirement:
            return 1
        if col < -requirement:
            return -1
    return 0

def extract_featuresets(ticker):
    tickers, df = process_data_for_labels(ticker)

    df['{}_target'.format(ticker)] = list(map(buy_sell_hold,
                                              df['{}_1d'.format(ticker)],
                                              df['{}_2d'.format(ticker)],
                                              df['{}_3d'.format(ticker)],
                                              df['{}_4d'.format(ticker)],
                                              df['{}_5d'.format(ticker)],
                                              df['{}_6d'.format(ticker)],
                                              df['{}_7d'.format(ticker)],
                                              ))
    vals = df['{}_target'.format(ticker)].values.tolist()
    str_vals = [str(i) for i in vals]
    print('Data spread:', str_vals)

    df.fillna(0, inplace=True)
    df = df.replace([np.inf, -np.inf], np.nan)
    df.dropna(0, inplace=True)

    df_vals = df[[ticker for ticker in tickers]].pct_change()
    df_vals = df_vals.replace([np.inf, -np.inf], 0)
    df_vals.fillna(0, inplace=True)

    X = df_vals.values
    y = df['{}_target'.format(ticker)].values

    return X, y, df

# extract_featuresets('XOM')

def do_ml(ticker):
    X, y, df = extract_featuresets(ticker)

    X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y, test_size=0.25)
    clf = neighbors.KNeighborsClassifier()

    clf.fit(X_train, y_train)
    confidence = clf.score(X_test, y_test)
    # Buy, sell, hold so 33% 'accuracy' would be the random outcome
    print('Accuracy:', confidence)
    predictions = clf.predict(X_test)
    print('Predicted spread:', predictions)

    return confidence

do_ml('AMZN')

Quant Analysis - grab s&p 500 data and perform analysis.py


# -*- coding: utf-8 -*-
"""
Created on Wed Apr 26 17:20:50 2023

@author: User
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

# define the probabilities of each value on the super dice
probabilities = [1/15,1/15,1/15,1/15,1/15,2/15,4/15,2/15,1/15,1/15]

# generate a random sample of 1000 rolls using the defined probabilities
sample = np.random.choice(range(1, 11), size=1000, p=probabilities)

# define the Gaussian function
def gaussian(x, A, mu, sigma):
    return A*np.exp(-(x-mu)**2/(2*sigma**2))

 # A*np.exp(-(x-mu)**2/(2*sigma**2))
# 1/(2*np.pi*sigma)*np.exp( -(x-mu)**2 / (2*sigma2) )

# def gaussian(x, mu, sigma):
#     return 1/(np.sqrt(2*np.pi)*sigma)*np.exp( -(x-mu)**2 / (2*sigma) )

# bin the sample data into a histogram
hist, bins = np.histogram(sample, bins=range(1, 12), density=True)

# calculate the center of each bin as x values for fitting
x = (bins[:-1] + bins[1:]) / 2

x_data = x[3:10]

# fit the Gaussian model to the histogram data
popt, cov = curve_fit(gaussian, x, hist)

A, mu, sigma = popt
# plot the histogram and fitted Gaussian curve
x_model = np.linspace(4.5, 10.5, 10)
y_model = gaussian(x, A, mu, sigma)
r=hist-y_model
plt.hist(sample, bins=range(1, 12), density=True, alpha=0.5, label='Sample data')
plt.plot(x, y_model, color='red', label='Fit')
plt.title('Gaussian Curve fitting')
plt.grid('on')
plt.xlabel('Dice Number')
plt.ylabel('Probability')
plt.legend()
plt.show()
plt.plot(x,r)
plt.scatter(x,r)
plt.xlabel('Dice Number')
plt.ylabel('Difference')
plt.title('Residual gaussian fit')
plt.grid('on')

# # er=np.sqrt(np.diag(pcov))
# yerr_data=np.sqrt(hist)
# plt.errorbar(x,hist,yerr_data, ls='', color='k')
# # plt.scatter(x_data, y_data)
# plt.plot.show()


import asyncio
import multiprocessing
from functools import partial

class CancellablePool(object):
    def __init__(self, max_workers=None):
        if max_workers is None:
            max_workers = max(multiprocessing.cpu_count() - 1, 1)
        self.max_workers = max_workers
        self._free = set()
        self._working = set()
        self._change = asyncio.Event()

    def _new_pool(self):
        return multiprocessing.Pool(1)

    async def apply(self, fn, *args):
        """
        Like multiprocessing.Pool.apply_async, but:
         * is an asyncio coroutine
         * terminates the process if cancelled
        """
        if len(self._free) + len(self._working) < self.max_workers:
            self._free.add(self._new_pool())

        while not self._free:
            await self._change.wait()
            self._change.clear()
        pool = usable_pool = self._free.pop()
        self._working.add(pool)

        loop = asyncio.get_event_loop()
        fut = loop.create_future()

        def save_fut_set_result(future, obj):
            if future.cancelled():
                return
            future.set_result(obj)

        def save_fut_set_exception(future, err):
            if future.cancelled():
                return
            future.set_exception(err)

        def _on_done(obj):
            loop.call_soon_threadsafe(partial(save_fut_set_result, future=fut, obj=obj))

        def _on_err(err):
            loop.call_soon_threadsafe(partial(save_fut_set_exception, future=fut, err=err))

        pool.apply_async(fn, args, callback=_on_done, error_callback=_on_err)

        cancel = False
        try:
            return await fut
        except asyncio.CancelledError as e:
            cancel = True
            raise e
        finally:
            if cancel:
                pool.terminate()
                self._working.remove(pool)
            else:
                self._working.remove(pool)
                self._free.add(usable_pool)
            self._change.set()

    def shutdown(self):
        for p in self._working | self._free:
            p.terminate()
        self._free.clear()


#!/usr/bin/env python
# coding: utf-8

# In[28]:

import matplotlib.pyplot as plt
import scipy.io as io
da = io.loadmat('Subject1_2D.mat')
x = da['LeftBackward1']
plt.plot(x)
plt.show()

# In[30]:

x1 = da['RightBackward1']
plt.plot(x1)
plt.show()

# In[5]:

da

# In[ ]:

random movements of left and right hand/random movements of left and right hand.py


# Investigative Query Visualizations

# In[1]:
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd

# In[2]:
# Time Series Analysis: Month,Year vs Avg AQI & Acres Burned

year_month = pd.read_csv('year_month.csv')

# In[3]:
def str_date(year, month):

    month = str(int(month))
    if len(month) == 1:
        month = '0'+ month
    return f'{str(int(year))}-{month}-01'

# In[4]:
year_month['date'] = year_month.apply(lambda row: str_date(row['year(date)'], row['month(date)']) , axis=1)

# In[5]:
year_month.head(2)

# In[6]:
# Create traces
fig = go.Figure()
fig.add_trace(go.Scatter(x=year_month['date'], y=year_month['avg(aqi)'],
                    mode='lines', marker_color='#2adea5',
                    name='Monthly Average AQI'))

fig.update_layout(title= 'Monthly Average AQI from 2013-2019',
                  xaxis_title='Year',
                  yaxis_title='AQI')
fig.show()

# In[7]:
fig = go.Figure()
fig.add_trace(go.Scatter(x=year_month['date'], y=year_month['acres_burned'],
                    mode='lines+markers', marker_color='#e8666d',
                    name='Monthly Acres Burned'))

fig.update_layout(title= 'Acres Burned per Month from 2013-2019',
                  xaxis_title='Year',
                  yaxis_title='Acres Burned')
fig.show()

# ### Scatterplot #1 : Percent of County Burned vs AQI Level

# In[8]:
pct_aqi = pd.read_csv('percent_vs_aqi.csv')

# In[9]:
pct_aqi['percent'] = pct_aqi.apply(lambda row: row['percent'] * 100, axis=1)

# In[10]:
pct_aqi.head(2)

# In[11]:
fig = px.scatter(pct_aqi, x="percent", y="avg(aqi)", hover_name='county_name', trendline="ols",
                 color_discrete_sequence = ['#e89835'],
                 labels = {'percent' : 'Percentage of the County that has Burned',
                           'avg(aqi)' : 'Averge AQI'},
                title = 'Percentage of County that has Burned vs Average AQI Level')
fig.show()

# In[12]:
# Scatterplot #2: Percent of County Burned vs Precipitation

acres_prcp = pd.read_csv('acrespcrp.csv')

# In[13]:
acres_prcp.head()

# In[14]:
acres_prcp['percent'] = acres_prcp.apply(lambda row: row['burned'] * 100, axis=1)

# In[15]:
acres_prcp.head(2)

# In[16]:
fig = px.scatter(acres_prcp, x="percent", y="sum(prcp)", hover_name='county',
                 color_discrete_sequence = ['#3461eb'],
                 labels = {'percent' : 'Cummulative Percentage of County that has Burned',
                           'sum(prcp)' : 'Total Precipitation'},
                title = 'Percentage of County that has Burned vs Total Precipitation')
fig.show()

# In[17]:
# Scatterplot #3: Acres Burned, AQI, County Population
burned = pd.read_csv('burnaqi.csv')

# In[18]:
burned.head()

# In[19]:
fig = px.scatter(burned, x="acres", y="aqi", hover_name='counties', trendline="ols",
                 size = 'population', color_discrete_sequence = ['#e33c17'], size_max = 50,
                 labels = {'acres' : 'Total Number of Acres Burned',
                           'aqi' : 'Averge AQI'},
                title = 'Number of Acres Burned vs Avg AQI Level by County')
fig.show()


import os
import subprocess
from distutils.command.build import build
from itertools import chain

from setuptools import find_packages, setup

package_name = "comics_net"

with open(os.path.join(os.getcwd(), "VERSION")) as version_file:
    version = version_file.read().strip()

def parse_requirements(file):
    with open(file, "r") as fs:
        return [
            r
            for r in fs.read().splitlines()
            if (
                len(r.strip()) > 0
                and not r.strip().startswith("#")
                and not r.strip().startswith("--")
            )
        ]

requirements = parse_requirements("requirements.txt")

def mypy_typecheck():
    err_msg = "[SKIP] WARNING: mypy is not installed -- *NOT* type checking"
    try:
        with open(os.devnull, "w") as FNULL:
            exit_code = subprocess.call(
                ["mypy", "-h"], stdout=FNULL, stderr=subprocess.STDOUT
            )
            if exit_code != 0:
                print(err_msg)
                return
    except FileNotFoundError:
        print(err_msg)
        return

    def list_files(directory):
        for dirpath, dirnames, filenames in os.walk(directory):
            for f in filenames:
                fpath = os.path.join(dirpath, f)
                if os.path.isfile(fpath):
                    yield fpath

    def py_file(filename):
        return filename.endswith(".py") or filename.endswith(".pyx")

    project_py_files = list(
        filter(
            lambda x: "setup.py" not in x,
            chain(*[filter(py_file, list_files(loc)) for loc in ["."]]),
        )
    )

    mypy_command = ["mypy", "--ignore-missing-imports"] + project_py_files

    print(f"Typechecking with mypy:")
    exit_code = subprocess.call(mypy_command)
    if exit_code != 0:
        print("\n\nERROR: type checking failed -- type errors detected")
        exit(exit_code)
    else:
        print("--> project checks out")

class BuildCmd(build):
    def run(self):
        mypy_typecheck()
        build.run(self)

setup(
    name=package_name,
    version=version,
    license="GNU General Public License",
    description="A deep learning utility for comic book covers",
    author="Sean MacRae",
    author_email="s.mac925@gmail.com",
    url="https://github.com/macrae/comics-net",
    packages=find_packages(exclude=["tests"]),
    cmdclass={"build": BuildCmd},
    install_requires=requirements,
    include_package_data=True,
    zip_safe=False,
)


from enum import Enum
from typing import Optional

from fastapi import Query

class FilmSortEnum(str, Enum):
    """Варианты сортировки"""
    imdb_rating_asc: str = 'imdb_rating'
    imdb_rating_desc: str = '-imdb_rating'

class Sort:
    """Добавление сортировки в параметры запроса"""

    def __init__(
        self,
        sort: FilmSortEnum = Query(
            FilmSortEnum.imdb_rating_desc,
            title='Сортировка',
            description='Сортирует по возрастанию и убыванию',
            alias='sort',
        ),
    ) -> None:
        self.sort = (sort,)

class Filter:
    """Добавление фильтрации в параметры запроса"""

    def __init__(
        self,
        _filter: Optional[str] = Query(
            None,
            title='Фильтр',
            description='Фильтрует по жанрам',
            alias='filter[genre]')
    ):
        self.filter = _filter


#!/usr/bin/env python
import roslib
from std_msgs.msg import String
from geometry_msgs.msg import Twist
import rospy
import serial
import struct
from time import sleep
import tf
roslib.load_manifest('rover_5_navigation')

ser = serial.Serial('/dev/ttyACM0', 9600)

# Function called when Twist messages are published
def navigation(data):
    logger.publish("Recieved")

    linVel = data.linear.x * 255 / 2 * 8
    rotVel = data.angular.z * 255 / 2 * 66
    # Combine, divide by radius, and scale to fit into an integer byte
    leftPower = round((linVel + rotVel))
    if leftPower >= 255:
        leftPower = 255
    if leftPower <= -255:
        leftPower = -255
    rightPower = round((linVel - rotVel))
    if rightPower >= 255:
        rightPower = 255
    if rightPower <= -255:
        rightPower = -255

    if leftPower >= 0:
        if rightPower >= 0:
           ser.write(struct.pack('>BBBB',76, int(leftPower), 82, int(rightPower)))
        else:
            ser.write(struct.pack('>BBBB',108, int(leftPower), 82, int(-rightPower)))
    else:
        if rightPower <= 0:
            ser.write(struct.pack('>BBBB',108, int(-leftPower),114, int(-rightPower)))
        else:
            ser.write(struct.pack('>BBBB',76, int(-leftPower), 114, int(rightPower)))

    # if rotational_power >= 0:
    #    # Sends 'Lr'
    #     ser.write(struct.pack('>BBBB',108, int(left_power), 82, int(rotational_power)))
    # else:
    #     # Sends 'lR'
    #     ser.write(struct.pack('>BBBB',76, int(-left_power), 114, int(-rotational_power)))

    # if forward_power >= 0:
    #     # Sends 'LR'
    #     ser.write(struct.pack('>BBBB',76, int(left_power), 82, int(forward_power)))
    # else:
    #     # Sends 'lr'
    #     ser.write(struct.pack('>BBBB',108, int(-left_power),114, int(-forward_power)))

if __name__ == '__main__':
    #Prep all the topics we want to publish/subscribe to
    logger = rospy.Publisher('logger', String, queue_size=10)

    rospy.Subscriber("cmd_vel", Twist, navigation)

    # Start the driver node
    rospy.init_node('base_controller')

    # Start node logging
    log_string = 'Starting base_controller....%s' % rospy.get_time()
    rospy.loginfo(log_string)
    logger.publish(log_string)

rospy.spin()


import time
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms
from torch.nn import functional as F
from matplotlib import pyplot as plt
from HaroldNet import LeNet
import matplotlib.image as mpimg  # mpimg 用于读取图片
import PIL.ImageOps
import requests
from PIL import Image
import numpy as np
from skimage.transform import resize

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
batch_size = 64
transform_train = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    transforms.RandomAffine(0, shear=10, scale=(0.8, 1.2)),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))
])

transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))
])

train_data = datasets.CIFAR10(root='../resources/MNIST', train=True, transform=transform_train, download=False)
test_dataset = datasets.CIFAR10(root='../resources/MNIST', train=False, transform=transform, download=False)
train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

def im_convert(tensor):
    image = tensor.cpu().clone().detach().numpy()  # This process will happen in normal cpu.
    image = image.transpose(1, 2, 0)
    image = image * np.array((0.5, 0.5, 0.5)) + np.array((0.5, 0.5, 0.5))
    image = image.clip(0, 1)
    return image

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torch.load('../models/CIFAR10/CIFAR10_20210118_1.pk1')  # 加载模型
model = model.to(device)
model.eval()

def evaluation(srcPath):
    src = mpimg.imread(srcPath)  # 读取和代码处于同一目录下的 图片
    # 此时 lena 就已经是一个 np.array 了，可以对它进行任意处理

    # resize至32x32
    newImg = resize(src, output_shape=(32, 32))
    plt.imshow(newImg)  # 显示图片
    # plt.axis('off')  # 不显示坐标轴
    plt.show()

    # 转换为（B,C,H,W）大小
    imagebatch = newImg.reshape(-1, 3, 32, 32)

    # 转换为torch tensor
    image_tensor = torch.from_numpy(imagebatch).float()

    # 调用模型进行评估
    model.eval()
    output = model(image_tensor.to(device))
    _, predicted = torch.max(output.data, 1)
    pre = predicted.cpu().numpy()
    print(pre)  # 查看预测结果ID
    print(classes[pre[0]])

def setEvaluation():
    dataiter = iter(test_loader)
    images, labels = dataiter.next()
    images = images.to(device)
    labels = labels.to(device)
    output = model(images)
    _, preds = torch.max(output, 1)

    fig = plt.figure(figsize=(25, 4))

    for idx in np.arange(0, 20, 1):
        # ax = fig.add_subplot(2, 10, idx + 1, xticks=[], yticks=[])
        ax = fig.add_subplot(2, 10, idx + 1, xticks=[], yticks=[])
        plt.imshow(im_convert(images[idx]))
        ax.set_title("{} ({})".format(str(classes[preds[idx].item()]), str(classes[labels[idx].item()])),
                     color=("green" if preds[idx] == labels[idx] else "red"))
    plt.plot()
    plt.show()

if __name__ == '__main__':
    setEvaluation()

    for seq in range(3, 4):
        srcPath = 'img/CIFAR_' + str(seq) + '.jpg'
        evaluation(srcPath)
        time.sleep(5)


from dataclasses import dataclass, field
import random
def randstat():
    return random.randint(0,10)

@dataclass
class Stats:
    combatskill: int = 0
    endurance: int = 0

    def __add__(self, other):
        combatskill = self.combatskill + other.combatskill
        endurance = self.endurance + other.endurance
        stats = Stats(combatskill, endurance)
        return stats

    def __remove__(self, other):
        combatskill = self.combatskill - other.combatskill
        endurance = self.endurance - other.endurance
        stats = Stats(combatskill, endurance)
        return stats

@dataclass
class Equipment:
    name: str
    description: str
    stats: Stats

@dataclass
class Player:
    stats: Stats
    equipment: list = field(default_factory=list)
    disciplines: list = field(default_factory=list)
    meals: int = 1
    crowns: int = randstat()

    @property
    def base_stats(self):
        self.combatskill = randstat() + 10
        self.endurance = randstat() + 20
        self.maxendurance = self.endurance

    @property
    def equip(self, equipment: Equipment):
        self.equipment.add(equipment)

    def drop(self, equipment: Equipment):
        self.equipment.remove(equipment)

@dataclass
class Enemy:
    name: str
    stats: Stats
    immunities: bool = False
    special_skills: list = field(default_factory=list)

@dataclass
class Encounter:
    text: str
    encounter_num: int
    ##'options' will be the number of choices for a given encounter. Potentially add a function that will print all of the choice options as defined by the 'Choice' class
    options: int

    #### optional skill check

    #### optional item check

    ### optional meal check

    meal_check: bool = False
    if meal_check and "Hunting" not in Player.disciplines:
        Player.endurance = Player.endurance - 3

    #### encounter options and associated text

    @dataclass
    class Choice:
        def check_skill(x):
            if x in Player.disciplines:
                pass
            else:
                print('You do not possess that skill')
        #choice_num will be the number the player needs to input
        choice_num: int
        text: str
        #'result' will run the encounter for the associated encounter number
        result: int
        skillcheck: bool = False
        if skillcheck:
            x = skillname
            check_skill(x)

    ### logic to proceed to next encounter

@dataclass
class Discipline:
    name: str
    description: str

    def discipline_check(self, name):
        if name == "Weaponskill":
            x = randstat()
            weaponlist = ["Dagger","Spear","Mace","Short Sword","Warhammer","Sword","Axe","Sword","Quarterstaff","Broadsword"]
            if weaponlist[x] in Player.equipment:
                Player.combatskill = Player.combatskill + 2
        elif name == "Healing":
            if Encounter.type != "combat" and Player.endurance < Player.maxendurance:
                Player.endurance +=1
        elif name == "Mindblast" and "Mindblast" not in Enemy.immunities:
            Player.combatskill = Player.combatskill + 2
        elif name == "Hunting":
            Encounter.meal_check = False
        elif name == "Mindshield":
            if "Mindblast" in Enemy.special_skills:
                Enemy.special_skills.remove("Mindblast")

##Disciplines - these will go into a different file later for legibility

hunting = Discipline("Hunting", "This skill ensures that a Kai Lord will never starve in the wild. He will always be able to hunt for food for himself except in areas of wasteland and desert. The skill also enables a Kai Lord to be able to move stealthily when stalking his prey.")
camouflage = Discipline("Camouflage", "This Discipline enables a Kai Lord to blend in with his surroundings. In the countryside, he can hide undetected among trees and rocks and pass close to an enemy without being seen. In a town or city, it enables him to look and sound like a native of that area, and can help him to find shelter or a safe hiding place.")

###   EXAMPLE ENCOUNTER  ###
e1 = Encounter("You must make haste for you sense it is not safe to linger by the smoking remains of the ruined monastery. The black-winged beasts could return at any moment. You must set out for the Sommlending capital of Holmgard and tell the King the terrible news of the massacre: that the whole of the Kai warriors, save yourself, have been slaughtered. Without the Kai Lords to lead her armies, Sommerlund will be at the mercy of their ancient enemy, the Darklords.\n\nFighting back tears, you bid farewell to your dead kinsmen. Silently, you promise that their deaths will be avenged. You turn away from the ruins and carefully descend the steep track.\n\nAt the foot of the hill, the path splits into two directions, both leading into a large wood.\n\n", 1, 3)

c1 = e1.Choice(1, "If you wish to use your Kai Discipline of Sixth Sense, turn to 141", 141, skillcheck = True)
c2 = e1.Choice(2, "If you wish to take the right path into the wood, turn to 85.", 85)
c3 = e1.Choice(3, "If you wish to follow the left track, turn to 275.", 275)

e85 = Encounter("The path is wide and leads straight into thick undergrowth. The trees are tall here and unusually quiet. You walk for over a mile when suddenly you hear the beating of large wings directly above you. Looking up, you are shocked to see the sinister black outline of a Kraan diving to attack you.", 85, 2)

######

if __name__ == "__main__":
    print(hunting.description)


"""
Various utilities for the REPL.

This module contains utilities for computing asymptotic path complexity.
It also allows us to execute a block of code such that an error will be thrown
if the execution takes too long by using the Timeout class.
"""

import os
import re
import signal
from collections import Counter
from types import FrameType, TracebackType
from typing import Dict, List, Optional, Tuple, Type, Union

import pycparser  # type: ignore
from mpmath import mpc, mpf, polyroots  # type: ignore
from pycparser import parse_file
from sympy import Abs, Basic, Mul, Poly, Pow, limit, symbols, sympify, simplify  # type: ignore

def get_solution_from_roots(roots: List[Union[mpf, mpc]]) -> Tuple[List[Basic], List[Basic]]:
    """Return the solution to a recurrence relation given roots of the characteristic equation."""
    # Round to 4 digits.
    new_roots = (complex(round(root.real, 6), round(root.imag, 6)) for root in roots)

    # Compute the multiplicy of each root.
    roots_with_multiplicities = Counter(new_roots)

    # Compute the coefficients of a_n as a list.
    solution = []
    simplified_solution = []
    for root in roots_with_multiplicities.keys():
        for i in range(roots_with_multiplicities[root]):
            if root == 1:
                term = sympify(f"(n**{i})")
                solution += [term]
                simplified_solution += [term]
            else:
                # root is actually 1/root, so (root)**n matches the paper
                solution += [sympify(f"(n**{i})*{root}**n")]
                abs_root = Abs(root)
                if abs_root == 1:
                    simplified_solution += [sympify(f"(n**{i})")]
                else:
                    simplified_solution += [sympify(f"(n**{i})*{Abs(root)}**n")]

    return solution, simplified_solution

def get_recurrence_solution(recurrence: str) -> Tuple[List[Basic], List[Basic]]:
    """
    Return the coefficients to a homogeneous linear recurrence relation.

    This is represented as a string of the format

    c_0*f(n) + c_1*f(n-1) + ... + c_k*f(n-k)

    by finding the roots of the characteristic equation.
    """
    # Regular expression to parse the recurrence relation expression.
    # RE matches a particular term: Coefficient + f(something)
    match_function = r'f\([ a-zA-Z0-9-+]*\)'
    match_obj = re.findall(rf'([ +-.0-9]*)(\*)({match_function})', recurrence)
    coeffs: List[float] = []
    for match in match_obj:
        coeffs += [float(match[0].replace(" ", ""))]

    # Normalize such that leading coefficient is 1.
    leading_coeff = coeffs[0]
    coeffs = list(map(lambda coeff: coeff / leading_coeff, coeffs))

    # Find the roots of the characteristic equation
    # through arbitrary precision root finding function.
    roots = polyroots(coeffs, maxsteps=200, extraprec=200)

    return get_solution_from_roots(roots)

def get_taylor_coeffs(func: Basic,
                      max_num_coeffs: int,
                      num_coeffs: int,
                      lazy: bool = True) -> Tuple[List[int], Optional[int]]:
    """Given an arbitrary rational function, get its Taylor series coefficients."""
    t_var = symbols('t')

    if lazy:
        taylor_series_generator = func.series(t_var, x0=0, n=None)

        first_nonzero_term = next(taylor_series_generator)
        if isinstance(first_nonzero_term, Pow):
            curr_term_exp = first_nonzero_term.exp
        else:
            # must be Mul or Symbol
            try:
                curr_term_exp = first_nonzero_term.args[1].exp
            except IndexError:
                curr_term_exp = 1
        new_start_idx = curr_term_exp
        taylor_series = [0] * curr_term_exp + \
            [first_nonzero_term.args[0] if isinstance(first_nonzero_term, Mul) else 1]
        taylor_series += list(map(
            lambda x: int(x.args[0]) if isinstance(x, Mul) else 1,
            [next(taylor_series_generator) for _ in range(num_coeffs)]
        ))
        # Workaround to get all coeffs, since sympy does not return 0 coeffs,
        # Make sure to return coeffs from low -> high order.
        return taylor_series, new_start_idx

    # Get the series but ignore the big-O term that is added by sympy.
    # Note: removeO() makes it go from high order -> low order
    # taylor_series = func.series(t_var, x0=0, n=max_num_coeffs).removeO()
    taylor_series = func.series(t_var, x0=0, n=max_num_coeffs).removeO()
    return Poly(taylor_series).all_coeffs()[::-1], None

def big_o(terms: List[str]) -> str:
    """
    Compute the big O of some expression in terms of 'n'.

    The terms should be a list of expressions represented as strings.
    """
    # print(terms)
    try:
        with Timeout(seconds = 200, error_message="Big O Timed Out"):
            n_var = symbols('n')
            if len(terms) == 0:
                return '0'

            if len(terms) == 1:
                return terms[0]

            term_one = terms[0]
            term_two = terms[1]

            lim = limit(Abs(sympify(term_two) / sympify(term_one)), n_var, float('inf'))
            # print(f"limit in big o: {lim}")

            if lim == 0: # second term strickly less than first term
                terms.remove(term_two)
                return big_o(terms)
            elif lim == float('inf'): # first term strickly less than second term
                terms.remove(term_one)
                return big_o(terms)
            else: #first and second term have the same power, add them
                # print("term one:",term_one)
                # print("term two:",term_two)
                terms[0] = simplify(term_one + term_two)
                # print("combined:",terms[0])
                terms.remove(term_two)
                return big_o(terms)

            return big_o(terms[1:]) # delete the first term
            # if lim < 1: # AFTER: if second term is smaller than first term (even by coefficients when the power is the same)
            #     terms.remove(term_two)
            #     return big_o(terms)

            # return big_o(terms[1:])
    except:
        return str(terms[0]) + " + " + str(big_o(terms[1:]))

def show_func_defs(filename: str) -> Dict[str, str]:
    """Return a list of the functions defined in a file."""
    # pylint: disable=unidiomatic-typecheck
    ast = parse_file(filename, use_cpp=True,
                     cpp_args=r'-I/app/pycparser/utils/fake_libc_include')

    names = {}
    for i in ast:
        if type(i) is pycparser.c_ast.FuncDef:
            names[i.decl.name] = str(i.decl.coord)
        elif type(i) is pycparser.c_ast.Decl:
            if i.name is not None:
                names[i.name + '_decl'] = str(i.coord)
    return names

def calls_function(calls_dict: Optional[dict[int, str]], function_cfg: str) -> list[int]:
    """Check if a CFG contains a call to another function."""
    nodes = []
    func_name = os.path.splitext(os.path.splitext(function_cfg)[0])[1][1:]
    if calls_dict is not None:
        for node in calls_dict.keys():
            if calls_dict[node] == function_cfg:
                nodes.append(node)
            if func_name == "":
                pass
            elif calls_dict[node].endswith(func_name):
                nodes.append(node)
    return nodes

class Timeout:
    """
    Set a maximum amount of a time a block of code can take to execute.

    Allows us to run a block of code such that a TimeoutError is thrown if
    it does not finish within a given amount of time.
    """

    def __init__(self, seconds: int = 1, error_message: str = 'Timeout') -> None:
        """Create a new instance of Timeout."""
        self.seconds = seconds
        self.error_message = error_message

    def handle_timeout(self, signum: int, frame: FrameType) -> None:
        """Execute after self.seconds have passed if the block within it is not done."""
        raise TimeoutError(self.error_message)

    def __enter__(self) -> None:
        """Start the timer when we begin executing the code within the block this class wraps."""
        signal.signal(signal.SIGALRM, self.handle_timeout)
        signal.alarm(self.seconds)

    def __exit__(self,
                 err_type: Optional[Type[BaseException]],
                 value: Optional[BaseException],
                 traceback: Optional[TracebackType]) -> None:
        """Stop the timer once the code block is done executing."""
        signal.alarm(0)


from typing import Any

class CF:
    """
    Class for all the constant and calculation related to certainty factor(CF)
    """
    TRUE = 1.0
    FALSE = -1.0
    UNKNOWN = 0.0
    CUTOFF = 0.2

    @staticmethod
    def cf_or(a: float, b: float):
        """
        :param a:
        :param b:
        :return: return the result of or(cf_a, cf_b)
        """
        if a > 0 and b > 0:
            return a + b - a * b
        if a < 0 and b < 0:
            return a + b + a * b
        return (a + b) / (1 - min(abs(a), abs(b)))

    @staticmethod
    def cf_and(a: float, b: float):
        """
        :param a:
        :param b:
        :return: return the result of and(cf_a, cf_b)
        """
        return min(a, b)

    @staticmethod
    def cf_valid(x: float):
        """
        :param x:
        :return: return if the certainty factor is valid
        """
        return CF.FALSE <= x <= CF.TRUE

    @staticmethod
    def cf_true(x):
        """
        :param x:
        :return: return if the certainty factor is recognized as true
        """
        return CF.cf_valid(x) and x > CF.CUTOFF

    @staticmethod
    def cf_false(x):
        """
        :param x:
        :return: return if the certainty factor is recognized as false
        """
        return CF.cf_valid(x) and x < (CF.CUTOFF - 1)

class Context:
    """
    Context
    """
    cnt = 0
    contexts = {}

    def __init__(self, name: str, initial_data: list[str] = [], goals: list[str] = []):
        if name in Context.contexts:
            raise ValueError(f'redundant context {name}')
        Context.contexts[name] = self
        self.id = Context.cnt
        Context.cnt += 1
        self.name = name
        self.initial_data = initial_data
        self.goals = goals

    def __str__(self):
        return f'Context {self.name}'

    @staticmethod
    def get_context_by_name(name):
        """
        find context by name
        :param name:
        """
        if name not in Context.contexts:
            raise ValueError(f'Context {name} not found.')
        return Context.contexts[name]

class Parameter:
    """
    Parameter is the property of a context. It is used as a type which does not contain
    any value
    """
    cnt = 0
    parameters = {}

    def __init__(self, name: str, ctx_name: str, ask_first: bool = False, param_type: type = None,
                 allowed_values: list = None):
        if name in Parameter.parameters:
            raise ValueError(f'redundant parameter with name {name}')
        Parameter.parameters[name] = self
        self.id = Parameter.cnt
        Parameter.cnt += 1
        self.name = name
        self.ask_first = ask_first
        self.ctx = Context.get_context_by_name(ctx_name)
        self.param_type = param_type
        self.allowed_values = allowed_values

    def __hash__(self):
        return hash(self.name)

    def __str__(self):
        return f'{self.name}'

    @staticmethod
    def get_parameter_by_name(name):
        """
        find parameter by name
        :param name:
        """
        if name not in Parameter.parameters:
            raise ValueError(f'Parameter {name} not found.')
        return Parameter.parameters[name]

    def get_value(self, val: Any):
        """
        :param val:
        :return: val of the same type of parameter or val that is within the allowed
        values
        """
        if self.param_type:
            return self.param_type(val)
        if val in self.allowed_values:
            return val
        raise ValueError('val must be one of %s for the parameter %s' %
                         (', '.join(list(self.enum)), self.name))

class Statement:
    """
    Statement: a statement can either be a condition or conclusion
    since they are in the same format (param, ctx, op, val)
    """
    cnt = 0

    def __init__(self, param_name: str, ctx_name: str, op_str: str, val: Any):
        self.param = Parameter.get_parameter_by_name(param_name)
        self.val = val
        self.context = Context.get_context_by_name(ctx_name)
        self.op_str = op_str
        if op_str == '>=':
            self.op = lambda x, y: x >= y
        elif op_str == '>':
            self.op = lambda x, y: x > y
        elif op_str == '<=':
            self.op = lambda x, y: x <= y
        elif op_str == '<':
            self.op = lambda x, y: x < y
        elif op_str == '=':
            self.op = lambda x, y: x == y
        else:
            raise ValueError(f'op_str for Condition can only be one of the following: <=, <, >=, >, =')

    def meet(self, real_val: Any):
        """
        :param real_val:
        :return: check if the statement meets
        it is only used for condition since conclusion does not need to be evaluated
        """
        val = self.param.get_value(self.val)
        real_val = self.param.get_value(real_val)
        return self.op(real_val, val)

    def __str__(self):
        return f'{self.param} {self.context.name} {self.op_str} {self.val}'

class Store:
    """
    Store: a global key/value store
    store_dict: (ctx, param) => {val: cf, ...}
    """
    store_dict = dict()

    @staticmethod
    def get_vals(ctx: Context, param: Parameter):
        """
        :param ctx:
        :param param:
        :return: val
        return the val dict according to ctx, param
        """
        if (ctx, param) not in Store.store_dict:
            Store.store_dict[(ctx, param)] = {}
        return Store.store_dict[(ctx, param)]

    @staticmethod
    def update_cf(ctx: Context, param: Parameter, val: Any, cf: float):
        """
        :param ctx:
        :param param:
        :param val:
        :param cf:
        update the stored cf
        """
        d = Store.get_vals(ctx, param)
        if val not in d:
            d[val] = CF.UNKNOWN
        cur_cf = d[val]
        Store.store_dict[(ctx, param)][val] = CF.cf_or(cur_cf, cf)

class Rule:
    """
    Rule: each Rule contains multiple conditions and conclusions, and an associated cf
    """
    rules = {}
    cnt = 0

    def __init__(self, conditions: list[tuple], conclusions: list[tuple], cf: float):
        self.id = Rule.cnt
        Rule.cnt += 1
        self.conditions = []
        for param_name, ctx_name, op, val in conditions:
            self.conditions.append(Statement(param_name, ctx_name, op, val))
        self.conclusions = []
        for param_name, ctx_name, op, val in conclusions:
            self.conclusions.append(Statement(param_name, ctx_name, op, val))
        self.cf = cf
        self._map_param_to_rules()

    def _map_param_to_rules(self):
        for condition in self.conclusions:
            if condition.param not in Rule.rules:
                Rule.rules[condition.param] = []
            Rule.rules[condition.param].append(self)

    def __str__(self):
        conditions = ' AND\n\t'.join([str(condition) for condition in self.conditions])
        conclusions = ' AND\n\t'.join(str(conclusion) for conclusion in self.conclusions)
        return f'RULE {self.id}: IF {conditions} THEN\n ({self.cf})\n {conclusions}\n'

    def applicable(self):
        """
        :return: cf related to the rule
        """
        for condition in self.conditions:
            cf = self.eval_condition(condition)
            if CF.cf_false(cf):
                return CF.FALSE

        total_cf = CF.TRUE
        for condition in self.conditions:
            Executor.find_out(condition.context.name, condition.param.name)
            cf = self.eval_condition(condition)
            total_cf = CF.cf_and(total_cf, cf)
            if not CF.cf_true(total_cf):
                return CF.FALSE
        return total_cf

    def apply(self):
        """
        :return: if the rule applied
        """
        Executor.set_current_rule(self)

        cf = self.cf * self.applicable()
        if not CF.cf_true(cf):
            return False

        for conclusion in self.conclusions:
            Store.update_cf(conclusion.context, conclusion.param, conclusion.val, cf)

        return True

    @staticmethod
    def use_rules(rules):
        """
        :param rules:
        :return: if the rules applied
        """
        applied = False
        for rule in rules:
            if rule.apply():
                applied = True
        return applied

    @staticmethod
    def eval_condition(condition):
        """
        :param condition:
        :return: the cf of all the conditions that meet
        """
        values = Store.get_vals(condition.context, condition.param)
        return sum(cf for known_val, cf in values.items() if condition.meet(known_val))

class Executor:
    """
    Executor: the executor of the whole program, it contains several local variable that will be used during the run time
    """
    known = set()
    asked = set()
    current_rule = None

    HELP = """Type one of the following:
     ?     - to see possible answers for this parameter
     rule  - to show the current rule
     why   - to see why this question is asked
     help  - to see this list
     xxx   - (for some specific xxx) if there is a definite answer
     (xxx .5 yyy .4) - If there are several answers with different certainty factors.
     """

    @staticmethod
    def ask(ctx: Context, param: Parameter):
        """
        Ask the user to provide the corresponding parameter
        return immediately if the parameter of the ctx is asked
        :param ctx:
        :param param:
        :return: the user's reply: str
        """

        def parse_user_input(param: Parameter, reply: str):
            """
            take in the user's input and parse it to [(val, CF.TRUE)]
            or [(val1, cf1), ...] if there are multiple answers
            :param param:
            :param reply:
            :return: list[tuple(Any, float)]
            """
            if reply.find(',') < 0:
                return [(param.get_value(reply), CF.TRUE)]
            vals = []
            for pair in reply.split(','):
                val, cf = pair.strip().split(' ')
                vals.append((param.get_value(val), float(cf)))
            return vals

        if (ctx, param) in Executor.asked:
            return
        Executor.asked.add((ctx, param))
        while True:
            resp = input(f'What is the {param.name} of {ctx.name}? ')
            if not resp:
                continue
            elif resp == 'unknown':
                return False
            elif resp == 'help':
                print(Executor.HELP)
            elif resp == 'why':
                Executor.explain(param)
            elif resp == 'rule':
                print(Executor.current_rule)
            elif resp == '?':
                if param.param_type:
                    print(f'{param.name} must be of type {param.param_type})')
                else:
                    print(f'the allowed values of {param.name} are {param.allowed_values}')
            else:
                try:
                    for val, cf in parse_user_input(param, resp):
                        Store.update_cf(ctx, param, val, cf)
                    return True
                except:
                    print('Invalid response. Type ? to see legal ones.')

    @staticmethod
    def explain(param):
        """
        print all the current known parameters and the rule being applied
        :param param:
        :return: None
        """
        print(f'Why is the value of {param.name} being asked for?')
        if Executor.current_rule in ('initial', 'goal'):
            print(f'{param.name} is one of the {Executor.current_rule} parameters.')
            return
        known, unknown = [], []
        for condition in Executor.current_rule.conditions:
            if CF.cf_true(Rule.eval_condition(condition)):
                known.append(condition)
            else:
                unknown.append(condition)

        if known:
            print('It is known that:')
            for condition in known:
                print(condition)
            print(f'Therefore, \n{Executor.current_rule}')

    @staticmethod
    def set_current_rule(rule):
        """
        Set the current rule of Executor class
        :param rule:
        """
        Executor.current_rule = rule

    @staticmethod
    def find_out(ctx_name: str, param_name: str):
        """
        find out the parameter indicated by the param_name
        :param ctx_name:
        :param param_name:
        :return: if the parameter is asked successfully: Bool
        """
        param = Parameter.get_parameter_by_name(param_name)
        ctx = Context.get_context_by_name(ctx_name)
        if (ctx, param) in Executor.known:
            return True

        success = False
        if param.ask_first:
            success = Executor.ask(ctx, param)

        if not success:
            if param in Rule.rules:
                success = Rule.use_rules(Rule.rules[param])
            if not success and not param.ask_first:
                success = Executor.ask(ctx, param)

        if success:
            Executor.known.add((ctx, param))
        return success

    @staticmethod
    def execute(context_names: list[str]):
        """
        entry point of the program
        :param context_names:
        :return: results
        """
        print(f'Executing begins, type HELP for help')
        results = {}
        for ctx_name in context_names:
            ctx = Context.get_context_by_name(ctx_name)

            Executor.set_current_rule('initial')
            for param_name in ctx.initial_data:
                Executor.find_out(ctx_name, param_name)

            Executor.set_current_rule('goal')
            for param_name in ctx.goals:
                Executor.find_out(ctx_name, param_name)

            if ctx.goals:
                result = {}
                for param_name in ctx.goals:
                    pram = Parameter.get_parameter_by_name(param_name)
                    result[param_name] = Store.get_vals(ctx, pram)
                results[ctx] = result
        Executor.output_result(results)
        return results

    @staticmethod
    def output_result(result):
        """
        output the findings of the program
        :param result:
        """
        for ctx, result in result.items():
            print(f'Findings for {ctx.name}:')
            for param_name, value_dict in result.items():
                possibilities = [f'{val[0]} {val[1]}' for val in value_dict.items()]
                possibilities.sort(key=lambda key: float(-float(key.split(' ')[-1])))
                possibilities_str = ', '.join(possibilities)
                print(f'{param_name}: {possibilities_str}')


import graphlab as gl
from . import data_preprocess as dp
from graphlab.toolkits.cross_validation import KFold
from graphlab.toolkits.model_parameter_search import grid_search
from . import nlp_funcs as nl
from collections import Counter
import pandas as pd
from . import scrape_leafly as sl
import matplotlib.pyplot as plt

def basic_fr(train, test):
    '''
    trains a default factorization_recommender on a train set and scores on
    test set

    args:
    train and test dataframes
    returns:
    graphlab recommendation engine
    '''
    test_og = test.copy()

    ratings = gl.SFrame(train)
    testsl = gl.SFrame(test)
    rec_engine = gl.factorization_recommender.create(observation_data=ratings,
                                                     user_id="user",
                                                     item_id="product",
                                                     target='rating',
                                                     solver='auto',
                                                     num_factors=32  # 32 by default
                                                     )

    test.rating = rec_engine.predict(testsl)
    test.to_csv('test_predictions.csv', index=False, encoding='utf-8')

    print('raw mse score:', dp.score_model_mse(test_og.rating, test.rating))

    return rec_engine

def gridsearch_big_step(df):
    '''
    gridsearches num_factors for the factorization_recommender in larger steps
    args: dataframe, df

    returns: gridsearch object
    '''
    data = gl.SFrame(df)
    kfolds = KFold(data, 3)
    params = dict([('user_id', 'user'),
                   ('item_id', 'product'),
                   ('target', 'rating'),
                   ('solver', 'auto'),
                   ('num_factors', [2, 3, 4, 5, 6, 10, 20, 32])])
    grid = grid_search.create(
        kfolds, gl.factorization_recommender.create, params)
    grid.get_results()

    return grid

    # results:

    '''
        +---------+-------------+--------+--------+---------+--------------+
    | item_id | num_factors | solver | target | user_id |   model_id   |
    +---------+-------------+--------+--------+---------+--------------+
    | product |      6      |  auto  | rating |   user  | [13, 12, 14] |
    | product |      2      |  auto  | rating |   user  |  [1, 0, 2]   |
    | product |      3      |  auto  | rating |   user  |  [3, 5, 4]   |
    | product |      4      |  auto  | rating |   user  |  [8, 7, 6]   |
    | product |      20     |  auto  | rating |   user  | [19, 18, 20] |
    | product |      32     |  auto  | rating |   user  | [21, 23, 22] |
    | product |      5      |  auto  | rating |   user  | [9, 11, 10]  |
    | product |      10     |  auto  | rating |   user  | [15, 17, 16] |
    +---------+-------------+--------+--------+---------+--------------+
    +----------------------+--------------------+------------------------+
    | mean_validation_rmse | mean_training_rmse | mean_training_recall@5 |
    +----------------------+--------------------+------------------------+
    |    1.00678522065     |   0.323030678029   |   0.000154761649416    |
    |     1.0208240936     |   0.453632730191   |   0.000122923478199    |
    |    1.00833928422     |   0.399805022552   |   0.000122507107597    |
    |    1.00770345896     |   0.362172915738   |   0.000107193018127    |
    |    0.983536655638    |   0.242915200506   |    0.00432907413023    |
    |    0.983353577078    |   0.24082851848    |    0.00435477863392    |
    |    1.00653732816     |   0.344224040089   |   0.000194937374462    |
    |    0.986200185618    |   0.272243232275   |    0.00193547952321    |
    +----------------------+--------------------+------------------------+
    +--------------------------+-----------+-----------------------------+-----------+
    | mean_validation_recall@5 |  fold_id  | mean_validation_precision@5 | num_folds |
    +--------------------------+-----------+-----------------------------+-----------+
    |    2.22170382466e-06     | [1, 0, 2] |      2.22170382466e-06      |     3     |
    |           0.0            | [1, 0, 2] |             0.0             |     3     |
    |           0.0            | [0, 2, 1] |             0.0             |     3     |
    |           0.0            | [2, 1, 0] |             0.0             |     3     |
    |    2.55287233685e-05     | [1, 0, 2] |      5.10991879673e-05      |     3     |
    |    3.18371310973e-05     | [0, 2, 1] |      3.77689650193e-05      |     3     |
    |           0.0            | [0, 2, 1] |             0.0             |     3     |
    |    1.58848655361e-05     | [0, 2, 1] |      1.55519267726e-05      |     3     |
    +--------------------------+-----------+-----------------------------+-----------+
    +---------------------------+
    | mean_training_precision@5 |
    +---------------------------+
    |     0.000139302381458     |
    |      0.0001082603732      |
    |     0.000133943316761     |
    |     0.000108451292331     |
    |      0.00392953856325     |
    |      0.00397506409151     |
    |      0.0001511569914      |
    |      0.00165102804133     |
    +---------------------------+
    '''

    '''
    looks like 20 features is a decent number.  need to gridsearch more later in the range 10-30
    '''

def gridsearch_alot(df):
    '''
    gridsearches num_factors for the factorization_recommender in range 2-32
    args: dataframe, df

    returns: gridsearch object
    '''
    data = gl.SFrame(df)
    kfolds = KFold(data, 3)
    num_factors_space = list(range(20, 31))
    num_factors_space.extend([2, 4, 6, 8, 10, 12, 16, 32])
    num_factors_space = sorted(num_factors_space)
    params = dict([('user_id', 'user'),
                   ('item_id', 'product'),
                   ('target', 'rating'),
                   ('solver', 'auto'),
                   ('num_factors', num_factors_space)])
    grid = grid_search.create(
        kfolds, gl.factorization_recommender.create, params)
    grid.get_results()

    return grid

if __name__ == "__main__":
    df = dp.load_data()
    df.drop(['date', 'time', 'review'], axis=1, inplace=True)
    df2 = dp.get_users_more_than_2_reviews(df)
    # # drop everything but user, product, rating
    grid2 = gridsearch_alot(df2)
    res2 = grid2.get_results()
    grid2.save('gridsearch_gt2_reviews.sframe')
    res2df = res2.to_dataframe() # converts to pandas dataframe
    res2df = res2df.sort_values(by='num_factors')
    res2df.to_csv('res2df.csv')
    res2df.plot(x='num_factors', y='mean_validation_rmse')
    plt.show()
    res2df.plot(x='num_factors', y='mean_training_rmse')
    plt.show()
    res2df.plot(x='num_factors', y='mean_training_recall@5')
    plt.show()
    res2df.plot(x='num_factors', y='mean_validation_recall@5')
    plt.show()
    res2df.plot(x='num_factors', y='mean_validation_precision@5')
    plt.show()
    res2df.plot(x='num_factors', y='mean_training_precision@5')
    plt.show()
    # looks like at about 10 factors, things flatten out

    # print grid.get_results()
    # remove user 'Anonymous' -- necessary to match up size of products from
    # data_preprocess get users and products func

    # basic rec engine first try
    #train, test = dp.make_tt_split(df)

    # trains and scores a basic factorization_recommender
    #basic_fr(train, test)

    # gridsearches over larger steps
    # gridsearch_big_step(df)

    # fit a model to the full review set to get latent feature groups

    # data = gl.SFrame(df)
    # num_factors = 20
    # rec_engine = gl.factorization_recommender.create(observation_data=data,
    #                                                  user_id="user",
    #                                                  item_id="product",
    #                                                  target='rating',
    #                                                  solver='auto',
    #                                                  num_factors=num_factors
    #                                                  )
    #
    # # assign groups to users and products based on highest coefficient in the matrices
    # d = rec_engine.get("coefficients")
    # U1 = d['user']
    # U2 = d['product']
    # U1 = U1['factors'].to_numpy()
    # U2 = U2['factors'].to_numpy()
    #
    # user_groups = U1.argmax(axis=1)
    # prod_groups = U2.argmax(axis=1)
    #
    # users, products = dp.get_users_and_products(df)
    #
    # full_df = dp.load_data()
    # prod_group_dict = {}
    # prod_group_dfs = {}
    # for i in range(num_factors):
    #     # this gets the product names in each group
    #     prod_list = products[prod_groups == i].index
    #     prod_group_dict[i] = prod_list
    #     prod_list = set(prod_list)
    #     # this will get dataframes from the main df with products in each group
    #     prod_group_dfs[i] = full_df[full_df['product'].isin(prod_list)]
    #
    # top_words = {}
    # top_words_set = set()
    # word_list = []
    # for i in range(num_factors):
    #     words = nl.get_top_words(prod_group_dfs[i])
    #     top_words_set = top_words_set | set(words)
    #     top_words[i] = words
    #     word_list.extend(words)
    #
    # word_counter = Counter(word_list)
    #
    # # try lemmatization
    # top_words = {}
    # top_words_set = set()
    # word_list = []
    # for i in range(num_factors):
    #     words = nl.get_top_words_lemmatize(prod_group_dfs[i], 50)
    #     top_words_set = top_words_set | set(words)
    #     top_words[i] = words
    #     word_list.extend(words)
    #
    # word_counter = Counter(word_list)

    '''
    gave this:

    ({u'also': 19,
         u'always': 1,
         u'anxiety': 20,
         u'atf': 1,
         u'awesome': 19,
         u'back': 2,
         u'beautiful': 1,
         u'bit': 1,
         u'blackberry': 1,
         u'blueberry': 1,
         u'body': 20,
         u'bubba': 1,
         u'bud': 20,
         u'buzz': 19,
         u'cheese': 2,
         u'chemdawg': 1,
         u'couch': 10,
         u'crack': 1,
         u'day': 20,
         u'definitely': 20,
         u'diesel': 1,
         u'earthy': 2,
         u'effect': 20,
         u'energetic': 3,
         u'energy': 1,
         u'euphoric': 13,
         u'ever': 20,
         u'far': 1,
         u'feel': 20,
         u'feeling': 20,
         u'felt': 14,
         u'first': 20,
         u'flavor': 20,
         u'fruity': 1,
         u'gdp': 1,
         u'get': 20,
         u'give': 18,
         u'go': 19,
         u'got': 20,
         u'green': 1,
         u'gsc': 1,
         u'ha': 20,
         u'haze': 2,
         u'headband': 1,
         u'heavy': 14,
         u'help': 7,
         u'hit': 20,
         u'hybrid': 1,
         u'indica': 20,
         u'insomnia': 1,
         u'jack': 1,
         u'kush': 8,
         u'lemon': 4,
         u'little': 19,
         u'long': 20,
         u'made': 19,
         u'make': 20,
         u'mellow': 14,
         u'much': 20,
         u'night': 15,
         u'og': 3,
         u'one': 20,
         u'orange': 1,
         u'perfect': 20,
         u'pineapple': 1,
         u'potent': 8,
         u'pretty': 20,
         u'purple': 2,
         u'recommend': 20,
         u'relaxed': 20,
         u'relief': 5,
         u'right': 2,
         u'sativa': 20,
         u'shit': 6,
         u'smoked': 20,
         u'smoking': 18,
         u'smooth': 20,
         u'still': 8,
         u'stress': 2,
         u'stuff': 20,
         u'super': 20,
         u'tasty': 1,
         u'top': 5,
         u'uplifting': 15,
         u'wa': 20,
         u'weed': 19,
         u'well': 20,
         u'white': 1,
         u'widow': 1,
         u'would': 20})

         words to use for feeling:
         uplifting, mellow, euphoric, euphoria, energy, energetic, relief
         words to use for taste:
         potent, pineapple, orange, lemon, earthy, diesel, blackberry, blueberry, cherry, banana
         words to use for conditions:
         insomnia, stress, headache, depression, anxiety
         words to use for effects:
         body, couch, mellow, focus, focused, cerebral
         times:
         daytime, night

         words to include in stoplist:
         weed, wa, well, white, widow, would, tasty, top, stuff, super, still, smooth, smoked,
         smoking, shit, sativa, right, relaxed, recommend, purple, pretty, og, one, perfect,
         far, feel, feeling, first, flavor, long, made, make, mellow, much, night, little, kush,
         jack, indica, hybrid, hit, help, heavy, headband, haze, ha, gsc, green, got, go, give,
         get, gdp, ever, always, atf, awesome, beautiful, bit, bubba, bud, buzz, chemdawg,
         crack, definitely, effect
    '''

    # try bigrams
    # top_words = {}
    # top_words_set = set()
    # word_list = []
    # for i in range(num_factors):
    #     words = nl.get_top_bigrams(prod_group_dfs[i])
    #     top_words_set = top_words_set | set(words)
    #     top_words[i] = words
    #     word_list.extend(words)
    #
    # word_counter = Counter(word_list)
    #
    # # get strain list so we can exclude it from bigrams
    # strains = sl.load_current_strains(correct_names=True)
    # strains_split = [s.split('/') for s in strains]
    # strain_info = [(s[1].lower(), s[2].lower(), re.sub('-', ' ', s[2].lower())) for s in strains_split]
    # # maps strain to category (hybrid, indica, sativa, edible) in each group
    # strain_cat_dict = {}
    # for s in strain_info:
    #     strain_cat_dict[s[1]] = s[0]
    #
    # # get % of each type (hybrid, indica, sativa, edible) in each group
    # prod_group_pcts = {}
    # for p in prod_group_dfs:
    #     temp_df = prod_group_dfs[p]
    #     temp_df['category'] = temp_df['product'].map(lambda x: strain_cat_dict[x])
    #     prod_group_dfs[p] = temp_df
    #     cat_val_counts_p = prod_group_dfs[p]['category'].value_counts()
    #     prod_group_pcts[p] = [round(float(c)/prod_group_dfs[p].shape[0]*100, 2) for c in cat_val_counts_p]
    #     prod_group_pcts[p] = pd.Series(data=prod_group_pcts[p], index=cat_val_counts_p.index)
    #
    #
    # # get % of each type overall
    # full_df['category'] = full_df['product'].map(lambda x: strain_cat_dict[x])
    # cat_val_counts = full_df['category'].value_counts()
    # print cat_val_counts
    # cat_val_pct = [round(float(c)/full_df.shape[0]*100, 2) for c in cat_val_counts]
    # cat_val_pct = pd.Series(data=cat_val_pct, index=cat_val_counts.index)
    #
    # print top_words_set # without any stopwords: [u'good', u'pain',
    # u'taste', u'high', u'strain', u'love', u'best', u'really', u'great',
    # u'like', u'favorite', u'smoke', u'time', u'smell', u'nice']


import configparser
import toml

# all version numbers should be:
version = open("../VERSION", 'r').read()
assert version != "0.0.0-development", "Please update the repository with the current version."
print("Checking if all version numbers are synchronized at", version)

# check that python version is set properly
config = configparser.RawConfigParser()
config.read('../python/setup.cfg')
assert config['metadata']['version'] == version, \
    "python/setup.cfg package version is incorrect"

# check that opendp crate version is set properly
opendp_toml = toml.load('../rust/opendp/Cargo.toml')
assert opendp_toml['package']['version'] == version, \
    "rust/opendp/Cargo.toml crate version is incorrect"

# check that opendp-ffi crate version is set properly
opendp_ffi_toml = toml.load('../rust/opendp-ffi/Cargo.toml')
assert opendp_ffi_toml['package']['version'] == version, \
    "rust/opendp-ffi/Cargo.toml crate version is incorrect"

assert opendp_ffi_toml['dependencies']['opendp']['version'] == version, \
    "rust/opendp-ffi/Cargo.toml opendp dependency is incorrect"

print("All version numbers are synchronized")


from scraper import Scraper
import json
from datetime import datetime
from pytz import timezone
import time

def main():
    """Update the news json file"""
    scraper = Scraper()
    data = scraper.scrape_all_news()

    bst = timezone('ASIA/DHAKA')
    data["updated_at"] = datetime.now(bst).strftime('%d/%m/%Y %H:%M:%S')

    with open('App/data.json', 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False)

    print(f"Scraper at {data['updated_at']}")

if __name__ == '__main__':
    while True:
        main()
        time.sleep(120)


"""
https://weblog.jamisbuck.org/archives.html
MAZE GENERATION using list and saving edges in tree. Note tree guarantees no cycles.
BUG:
    - Double A* cheats! But it seems to predict which is the correct path: Seed: 24769 / size: 20
      SOLUTION: Bad indenting. Did not check if maze.connected(B, neighbor), lol.
"""
import random
import pygame as pg
from queue import Queue
import networkx as nx
from Maze_config import *
import csv

DIRECTIONS = [(0, -1), (0, 1), (-1, 0), (1, 0)]  # up, down, left, right

CELL = SCREEN_SIZE // SIZE
WIDTH = 1
BLACK, GREEN, TEAL = (20, 20, 20), (20, 120, 20), (20, 70, 20)
BLUE, RED = (40, 40, 170), (170, 40, 40)

pg.init()
clock = pg.time.Clock()

screen = pg.display.set_mode((SCREEN_SIZE, SCREEN_SIZE))
pg.display.set_caption("Maze")

class Display:
    """Draws all information from Maze to the screen
    - screen - main screen
    - fill_screen - that path traveled thus far
    - maze_screen - constant layout of maze
    - player_screen - player circle
    - heat_screen - heat map
    """

    def __init__(self, rule_30):
        # self.graph = graph

        # Screens
        self.screen = pg.display.set_mode((SCREEN_SIZE, SCREEN_SIZE))
        pg.display.set_caption("Maze")

        self.fill_screen = pg.Surface((SCREEN_SIZE, SCREEN_SIZE))
        # self.fill_screen.fill(BLACK)
        self.fill_screen.fill(GREEN)
        # pg.draw.rect(self.fill_screen, TEAL, (START[0]*CELL, START[1]*CELL, CELL, CELL))
        # pg.draw.rect(self.fill_screen, GREEN, (GOAL[0]*CELL, GOAL[1]*CELL, CELL, CELL))

        self.maze_screen = pg.Surface((SCREEN_SIZE, SCREEN_SIZE))
        self.maze_screen.set_colorkey((0, 0, 0))

        # heat_map = [[1 for _ in range(SIZE)] for _ in range(SIZE)]
        self.heat_screen = pg.Surface((SCREEN_SIZE, SCREEN_SIZE))
        self.heat_screen.set_colorkey((0, 0, 0))

        self.player_screen = pg.Surface((CELL, CELL))
        pg.draw.circle(self.player_screen, GREEN, (CELL//2, CELL//2), CELL//4)
        self.player_screen.set_colorkey((0, 0, 0))

        # Draw the graph into appropriate screens
        for i in range(SIZE+1): # First draw all walls then remove walls between nodes in Maze
            pg.draw.line(self.maze_screen, GREEN, (0, i * CELL), (SCREEN_SIZE, i * CELL), WIDTH)
            pg.draw.line(self.maze_screen, GREEN, (i * CELL, 0), (i * CELL, SCREEN_SIZE), WIDTH)

        self.screen.blit(self.maze_screen, (0, 0))
        pg.display.update()

        if rule_30:
            a = SIZE//2 * '0' + '1' + SIZE//2 * '0'
            # r = random.randrange(SIZE)
            # a = a[:r] + '1' + a[r+1:]
            result = [list(map (int, a))]

            while a[0] != '1' and a[-1] != '1':
                a = self.rule30(a)
                result.append(list(map (int, a)))
                # result.append(a)
            for y, row in enumerate(result):
                for x, r in enumerate(row):
                    if not r:
                        continue
                    self.fill_screen.fill(TEAL, (x*CELL, y*CELL, CELL, CELL))
                    self.screen.blit(self.fill_screen, (0, 0))
                pg.display.update()
                process_input()
                # clock.tick(FPS)

        # print(result)  # list of lists
        # print(np.array(result))  # np.array

    def rule30(self, a):
        patterns = {'111': '0', '110': '0', '101': '0', '100': '1',
                    '011': '1', '010': '1', '001': '1', '000': '0', }  # rule 30
        # patterns = {'111': '0', '110': '1', '101': '0', '100': '1',
        #             '011': '1', '010': '0', '001': '1', '000': '0', }  # rule 90 (siepinsky triangle)
        # patterns = {'111': '0', '110': '1', '101': '1', '100': '0',
        #             '011': '1', '010': '1', '001': '1', '000': '0', }  # rule 110
        # patterns = {'111': '1', '110': '0', '101': '1', '100': '1',
        #             '011': '1', '010': '0', '001': '0', '000': '0', }  # rule 184
        a = '0' + a + '0'  # padding
        return ''.join([patterns[a[i:i+3]] for i in range(len(a)-2)])

    def draw_maze(self, graph):
        for edge in graph.edges:
            (x1, y1), (x2, y2) = edge
            if x1 == x2: # horizontal wall
                y = max(y1, y2)
                a = (x1 * CELL, y * CELL)
                b = (x1 * CELL + CELL, y * CELL)
            else: # vertical wall
                x = max(x1, x2)
                a = (x * CELL, y1 * CELL)
                b = (x * CELL, y1 * CELL + CELL)
            pg.draw.line(self.maze_screen, (0, 0, 0), a, b, WIDTH)

        # for i in range(SIZE+1):
        #     for j in range(SIZE+1): # Draw poles at every corner
        #         pg.draw.circle(self.maze_screen, GREEN, (i*CELL, j*CELL), WIDTH)

        self.update_screen(START)

    def update_screen(self, pos=None):
        self.screen.blit(self.fill_screen, (0, 0))
        # min-max normalize heat_map
        # show_heat_map()

        self.screen.blit(self.maze_screen, (0, 0))
        if pos:
            self.screen.blit(self.player_screen, (pos[0]*CELL, pos[1]*CELL))
        pg.display.update()

    def draw_path(self, path):
        x, y = path[0]
        for xp, yp in path[1:]:
            a = ((x + 0.5) * CELL, (y + 0.5) * CELL)
            b = ((xp + 0.5) * CELL, (yp + 0.5) * CELL)
            pg.draw.line(self.fill_screen, RED, a, b, CELL // 4)
            x, y = xp, yp
            self.update_screen()
            process_input()
            clock.tick(FPS)
        self.update_screen()

    def carve_path(self, a, b):
        self.fill_screen.fill(BLACK, (a[0]*CELL, a[1]*CELL, CELL, CELL))
        self.fill_screen.fill(BLACK, (b[0]*CELL, b[1]*CELL, CELL, CELL))
        self.screen.blit(self.fill_screen, (0, 0))

        (x1, y1), (x2, y2) = a, b
        if x1 == x2: # horizontal wall
            y = max(y1, y2)
            a = (x1 * CELL, y * CELL)
            b = (x1 * CELL + CELL, y * CELL)
        else: # vertical wall
            x = max(x1, x2)
            a = (x * CELL, y1 * CELL)
            b = (x * CELL, y1 * CELL + CELL)
        pg.draw.line(self.maze_screen, (0, 0, 0), a, b, WIDTH)

        self.screen.blit(self.maze_screen, (0, 0))
        pg.display.update()
        clock.tick(FPS)
        process_input()

DIRS = [-SIZE, SIZE, -1, 1] # up, down, left, right
class Maze:
    """Handles maze generation and intereaction.
    Uses Display class to draw the maze."""
    def __init__(self):
        self.display = Display(False) if DISPLAY else None
        self.graph = nx.Graph()
        self._create_maze(MAZE_GENERATOR)
        if DISPLAY:
            self.display.draw_maze(self.graph)

    def _create_maze(self, generator):
        if generator == 0:
            return self._stack()
        elif generator == 1:
            return self._eller()
        elif generator == 2:
            return self._wilson()
        elif generator == 3:
            return self._kruskal()
        elif generator == 4:
            return self._prim()
        elif generator == 5:
            return self._hunt_and_kill()
        else:
            raise ValueError(f"Unknown generator: {generator}")

    def _stack(self):
        grid = [[0 for _ in range(SIZE)] for _ in range(SIZE)]
        x, y = random.randrange(SIZE), random.randrange(SIZE)
        grid[y][x] = 1

        stack = [(x, y)]
        while stack:
            x, y = stack.pop(-1)

            unvisited = []
            for dir in DIRECTIONS:
                xp = x + dir[0]
                yp = y + dir[1]
                if 0 <= xp < SIZE and 0 <= yp < SIZE and not grid[yp][xp]:
                    unvisited.append((xp, yp))

            if unvisited:
                stack.append((x, y))
                xp, yp = unvisited.pop(random.randrange(len(unvisited)))

                self._connect((x, y), (xp, yp))
                grid[yp][xp] = 1
                stack.append((xp, yp))

    def _eller(self):
        # graph = nx.Graph()  # edges are added horizontally at 2) + 6) and vertically at 3)

        # 1. Initialize the cells of the first row to each exist in their own set.
        # grid = [[0 for _ in range(SIZE)] for _ in range(SIZE)]  # Contains info about sets
        # grid[0] = list(range(SIZE)) # initally all unique
        counter = SIZE
        row = list(range(SIZE))
        # 5. Repeat until the last row is reached.
        for y in range(SIZE-1):

            # 2. Now, randomly join adjacent cells, but only if they are not in the same set.
            for x in range(SIZE-1):
                if row[x] != row[x + 1] and random.random() < 0.5:
                    row[x + 1] = row[x]
                    self._connect((x, y), (x+1, y))
                    # graph.add_edge()

            # 3. For each set, randomly create vertical connections downward to the next row.
            # 4. Flesh out the next row by putting any remaining cells into their own sets.
            next_row = [0 for _ in range(SIZE)]
            for x in range(SIZE):
                if random.random() < 0.5:
                    next_row[x] = row[x]
                    self._connect((x, y), (x, y+1))
                    # graph.add_edge((x, y), (x, y+1))
                else:
                    next_row[x] = counter
                    counter += 1

            # 3.1. Ensure that each set has at least one downward connection.
            unexpanded = set(row) - set(next_row)
            for u in unexpanded:
                positions = [i for i, x in enumerate(row) if x == u]
                pos = random.choice(positions)
                next_row[pos] = u
                self._connect((pos, y), (pos, y+1))
                # graph.add_edge((pos, y), (pos, y+1))

            row = next_row

        # return grid

        # 6. For the last row, join all adjacent cells that do not share a set, and omit the vertical connections, and you’re done!
        y += 1
        for x in range(SIZE-1):
            if row[x] != row[x + 1]:
                self._connect((x, y), (x+1, y))

    def _kruskal(self):
        grid = [[0 for _ in range(SIZE)] for _ in range(SIZE)]
        sets = [set([(x, y)]) for x in range(SIZE) for y in range(SIZE)]
        coords = [(x, y) for x in range(SIZE) for y in range(SIZE)]
        random.shuffle(coords)

    def _wilson(self):
        maze = set()
        cells = [(x, y) for x in range(SIZE) for y in range(SIZE)]

        cell = cells.pop(random.randrange(len(cells)))
        maze.add(cell)

        while cells:
            # Choose arbitrary cell to start path from
            cell = cells.pop(random.randrange(len(cells)))
            if cell in maze:
                continue

            path = self._random_walk(cell, maze)
            maze.update(path)

            last = path.pop(0)
            for p in path:
                self._connect(last, p)
                last = p

    def _random_walk(self, cell, maze):
        path = [cell]
        while True:
            dir = random.choice(DIRECTIONS)
            xp = cell[0] + dir[0]
            yp = cell[1] + dir[1]
            if xp < 0 or xp >= SIZE or yp < 0 or yp >= SIZE:
                continue
            if (xp, yp) in maze:  # Path entered maze
                path.append((xp, yp))
                return path
            if (xp, yp) in path:  # No loops!
                index = path.index((xp, yp))
                path = path[:index+1]
                cell = path[-1]
                continue
            path.append((xp, yp))
            cell = (xp, yp)

    def _prim(self):
        maze = set()
        cells = [(x, y) for x in range(SIZE) for y in range(SIZE)]
        grid = [[0 for _ in range(SIZE)] for _ in range(SIZE)]

        cell = cells.pop(random.randrange(len(cells)))
        maze.add(cell)
        grid[cell[1]][cell[0]] = 1

        stack = set(self._get_neighbors(cell, grid))
        while stack:
            cell = random.choice(stack)
            # If only one of the cells that the wall divides is visited
            num_visited = 0
            for neighbor in self._get_neighbors(cell, grid):
                if grid[neighbor[1]][neighbor[0]]:
                    num_visited += 1

    def _get_neighbors(self, cell, grid):
        walls = []
        for dir in DIRECTIONS:
            xp = cell[0] + dir[0]
            yp = cell[1] + dir[1]
            if xp < 0 or xp >= SIZE or yp < 0 or yp >= SIZE or grid[yp][xp]:
                continue
            walls.append((cell, (xp, yp)))
        return walls

    def _hunt_and_kill(self):
        grid = [[0 for _ in range(SIZE)] for _ in range(SIZE)]

        # 1. Choose a starting location.
        pos = (random.randrange(SIZE), random.randrange(SIZE))
        grid[pos[1]][pos[0]] = 1

        # 4. Repeat steps 2-3 until all locations have been visited. (Hunt can't find any unvisited cells)
        while True:
            # 2. Perform a random walk, carving passages to unvisited neighbors, until it is unable to move.
            while True:
                directions = DIRECTIONS.copy()
                random.shuffle(directions)
                for dir in directions:
                    xp = pos[0] + dir[0]
                    yp = pos[1] + dir[1]
                    if xp < 0 or xp >= SIZE or yp < 0 or yp >= SIZE or grid[yp][xp]:
                        continue

                    self._connect(pos, (xp, yp))
                    grid[yp][xp] = 1
                    pos = (xp, yp)
                    break
                else:  # directions exhausted
                    break

            # 3. Hunt for unvisited cell with visited neighbor.
            pos, visited = self._hunt(grid)
            if pos is None:
                return

            grid[pos[1]][pos[0]] = 1
            self._connect(pos, visited)

    def _hunt(self, grid):
        for x in range(SIZE):
            for y in range(SIZE):
                if grid[y][x]:
                    continue

                for dir in DIRECTIONS:
                    xp = x + dir[0]
                    yp = y + dir[1]
                    if not self.in_bounds(xp, yp) or not grid[yp][xp]:
                        continue

                    return (x, y), (xp, yp)
        return None, None

    def in_bounds(self, x, y):
        return 0 < x < SIZE and 0 < y < SIZE

    def step(self, pos, color=TEAL):
        inside.add(pos)
        if DISPLAY:
            self.display.fill_screen.fill(color, (pos[0]*CELL, pos[1]*CELL, CELL, CELL))
            self.display.update_screen()
        clock.tick(FPS)
        process_input()

    def player_step(self, pos):
        if DISPLAY:
            self.display.fill_screen.fill(TEAL, (pos[0]*CELL, pos[1]*CELL, CELL, CELL))
            self.display.update_screen(pos)
        process_input()
        clock.tick(FPS)

    def connected(self, a, b):
        # return random.random() < 0.5
        return a in self.graph[b]
        return self.graph.has_edge(a, b)

    def _connect(self, a, b):
        self.graph.add_edge(a, b)
        if DISPLAY:
            self.display.carve_path(a, b)

    def draw_path(self, path):
        self.display.draw_path(path)

def flood_fill(start):
    Q = Queue()
    Q.put(start)
    while not Q.empty():
        pos = Q.get()
        # if pos == GOAL:
        #     return True

        maze.step(pos)

        x, y = pos

        if y > 0 and maze.connected((x, y), (x, y-1)) and not (x, y-1) in inside: # and inside
            Q.put((x, y-1))
        if y < SIZE-1 and maze.connected((x, y), (x, y+1)) and not (x, y+1) in inside:
            Q.put((x, y+1))
        if x > 0 and maze.connected((x, y), (x-1, y)) and not (x-1, y) in inside:
            Q.put((x-1, y))
        if x < SIZE-1 and maze.connected((x, y), (x+1, y)) and not (x+1, y) in inside:
            Q.put((x+1, y))

# import default dict
from collections import defaultdict
def A_star(start, goal):
    count = 0
    open_set = set()
    open_set.add(start)
    came_from = {}
    g_score = defaultdict(lambda: float("inf"))
    g_score[start] = 0
    f_score = defaultdict(lambda: float("inf"))
    f_score[start] = heuristic(start, goal)

    while len(open_set) > 0:
        count += 1
        current = min(open_set, key=lambda x: f_score[x])
        if current == goal:
            if DISPLAY:
                maze.step(current)
                reconstruct_path(came_from, current)
            return count
        maze.step(current)

        open_set.remove(current)
        for dir in DIRECTIONS:
            neighbor = (current[0] + dir[0], current[1] + dir[1])
            if 0 <= neighbor[0] < SIZE and 0 <= neighbor[1] < SIZE and maze.connected(current, neighbor):
                tentative_g_score = g_score[current] + 1  # Redundant, since all positions are visited only once
                if tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = tentative_g_score + heuristic(neighbor, goal)
                    open_set.add(neighbor)

def heuristic(pos, goal):  # manhattan distance
    (x, y), (gx, gy) = pos, goal
    return abs(x - gx) + abs(y - gy)

def reconstruct_path(came_from, current, reverse=False):
    # Determine path, then draw it
    path = [current]
    while current in came_from:
        current = came_from[current]  # start has no came_from so terminates there
        path.append(current)

    if reverse:
        path.reverse()

    maze.draw_path(path)

# Flip between two agents initially seeking the goal and star respectively
def A_star_double(start, goal):
    count = 0
    open_set_A = set([start])
    open_set_B = set([goal])
    came_from_A = {}
    came_from_B = {}
    g_score_A = defaultdict(lambda: float("inf"))
    g_score_A[start] = 0
    g_score_B = defaultdict(lambda: float("inf"))
    g_score_B[goal] = 0
    f_score_A = defaultdict(lambda: float("inf"))
    f_score_A[start] = heuristic(start, goal)
    f_score_B = defaultdict(lambda: float("inf"))
    f_score_B[goal] = heuristic(goal, start)

    # while len(open_set) > 0:
    while True:
        count += 1
        A = min(open_set_A, key=lambda x: f_score_A[x])
        maze.step(A)

        open_set_A.remove(A)

        for dir in DIRECTIONS:
            neighbor = (A[0] + dir[0], A[1] + dir[1])
            if 0 <= neighbor[0] < SIZE and 0 <= neighbor[1] < SIZE and maze.connected(A, neighbor):
                tentative_g_score = g_score_A[A] + 1
                if tentative_g_score < g_score_A[neighbor]:
                    came_from_A[neighbor] = A
                    g_score_A[neighbor] = tentative_g_score
                    f_score_A[neighbor] = tentative_g_score + heuristic(neighbor, goal)
                    if neighbor not in open_set_A:
                        open_set_A.add(neighbor)

                if neighbor in open_set_B:
                    if not DISPLAY:
                        return count
                    maze.step(neighbor, RED)
                    # increment_heat_map(neighbor)
                    reconstruct_path(came_from_B, neighbor, True)
                    reconstruct_path(came_from_A, neighbor)
                    return count
        count += 1
        B = min(open_set_B, key=lambda x: f_score_B[x])
        maze.step(B)

        open_set_B.remove(B)
        for dir in DIRECTIONS:
            neighbor = (B[0] + dir[0], B[1] + dir[1])
            if 0 <= neighbor[0] < SIZE and 0 <= neighbor[1] < SIZE and maze.connected(B, neighbor):
                tentative_g_score = g_score_B[B] + 1
                if tentative_g_score < g_score_B[neighbor]:
                    came_from_B[neighbor] = B
                    g_score_B[neighbor] = tentative_g_score
                    f_score_B[neighbor] = tentative_g_score + heuristic(neighbor, goal)
                    if neighbor not in open_set_B:
                        open_set_B.add(neighbor)

                if neighbor in open_set_A:
                    if not DISPLAY:
                        return count
                    maze.step(neighbor, RED)
                    increment_heat_map(neighbor)
                    reconstruct_path(came_from_B, neighbor, True)
                    reconstruct_path(came_from_A, neighbor)
                    return count

def increment_heat_map(index):
    return
    global min_heat, max_heat
    x, y = index % SIZE, index // SIZE
    if heat_map[index % SIZE][index // SIZE] == min_heat:
        min_heat += 1
    heat_map[index % SIZE][index // SIZE] += 1
    max_heat = max(max_heat, heat_map[y][x])

def process_input():
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
            exit()
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_ESCAPE:
                pg.quit()
                exit()

            if event.key == pg.K_SPACE:
                return "yikes"
            if event.key == pg.K_p:

                while True:
                    event = pg.event.wait()
                    if event.type == pg.QUIT:
                        pg.quit()
                        exit()
                    if event.type == pg.KEYDOWN:
                        if event.key == pg.K_ESCAPE:
                            pg.quit()
                            exit()
                    if event.type == pg.KEYDOWN and event.key == pg.K_SPACE:
                        break

def pause(t=0):
    if t:
        pg.time.delay(t*1000)
        return
    while True:
        if process_input():
            print("unpause")
            return
        clock.tick(60)

# def show_heat_map():
#     heat_screen.fill(BLACK)
#     process_input()
#     for y in range(SIZE):
#         for x, r in enumerate(heat_map[y]):
#             red = (r - min_heat) / (max_heat - min_heat)
#             heat_screen.fill((red, 0, 0), (x*CELL, y*CELL, CELL, CELL))
#     screen.blit(heat_screen, (0, 0))

def restart(seed=0):
    if seed:
        random.seed(seed)
    maze = Maze()
    inside = set(START)  # set of (x, y) that have been visited

    return maze, inside, START

single_steps_avg = 0
double_steps_avg = 0
iteration = 1
table = []
if not PLAYER:
    while True:
        # seed a random number
        seed = random.randrange(10e100)
        # seed = 48063
        maze, inside, (px, py) = restart(seed)
        # pause(2)
        if SOLVER == 0:
            flood_fill(START)
        elif SOLVER == 1:
            A_star(START, GOAL)
            # pause(2)
        elif SOLVER == 2:
            single_result = A_star(START, GOAL)
            single_steps_avg += 1/iteration * (single_result - single_steps_avg)

            maze, inside, (px, py) = restart(seed)
            double_result = A_star_double(START, GOAL)
            double_steps_avg += 1/iteration * (double_result - double_steps_avg)

            print(iteration, round(single_steps_avg, 2), round(double_steps_avg, 2))
            iteration += 1
            table.append([seed, single_result, double_result])
            with open("Maze_out.csv", "w", newline="") as f:
                writer = csv.writer(f)
                writer.writerows(table)
else:
    import numpy as np

    display = Display(True)

    # seed = 48063
    # maze, inside, (px, py) = restart()
    # maze.draw_path(PATH)
    # pause()
    while True:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                pg.quit()
                exit()

            if event.type == pg.KEYDOWN:
                if event.key == pg.K_ESCAPE:
                    pg.quit()
                    exit()
                elif event.key == pg.K_r:
                    maze, inside, (px, py) = restart()

                if event.key == pg.K_UP:
                    if py > 0 and maze.connected((px, py), (px, py-1)):
                        py -= 1
                elif event.key == pg.K_DOWN:
                    if py < SIZE-1 and maze.connected((px, py), (px, py+1)):
                        py += 1
                elif event.key == pg.K_LEFT:
                    if px > 0 and maze.connected((px, py), (px-1, py)):
                        px -= 1
                elif event.key == pg.K_RIGHT:
                    if px < SIZE-1 and maze.connected((px, py), (px+1, py)):
                        px += 1

                if (px, py) == GOAL:
                    maze, inside, (px, py) = restart()
                    continue
                # Update position
                # if (px, py) not in inside:
                maze.player_step((px, py))
                inside.add((px, py))

        clock.tick(60)


from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

browser = webdriver.Chrome()
browser.get("https://flight.naver.com/flights/")

browser.find_element_by_link_text("가는날 선택").click()

# 이번 달 27, 28일
browser.find_elements_by_link_text("27")[0].click()
browser.find_elements_by_link_text("28")[0].click()

# 다음 달 27, 28일
# browser.find_elements_by_link_text("27")[1].click()
# browser.find_elements_by_link_text("28")[1].click()

# 제주도 선택
browser.find_element_by_link_text("도착").click()
browser.find_element_by_xpath("//*[@id='l_1']/div/div[2]/div[2]/table[1]/tbody/tr[1]/td[1]/a").click()

# 항공권 검색 클릭
browser.find_element_by_link_text("항공권 검색").click()

# 로딩 기다려서 결과 출력하기
try:
    elem = WebDriverWait(browser, 10).until(EC.presence_of_element_located((By.XPATH, "//*[@id='content']/div[2]/div/div[4]/ul/li[1]")))
    print(elem.text)
finally:
    browser.quit()