# unicornhybridblack: classes to communicate with g.tec Unicorn Hybrid Black
#
"""
UnicornBlackThreads provides direct access to the thread functions

UnicornBlackProcess provides the ability to run the device as a seperate process

UnicornBlackCheckSignal provides a class to evaluate signal quality

Working notes
- Sometimes works when run in Spyder, but seems to work best when run in external

@author: Matt Pontifex
"""

import os
import math
import numpy
import time
from datetime import datetime
from threading import Thread, Lock
from multiprocessing import Queue
import multiprocessing
import matplotlib.mlab as mlab
import scipy.signal

# DEBUG #
if __name__ == "__main__":
    try:
        import UnicornPy as UnicornPy
    except:
        import Engine.UnicornPy as UnicornPy
else:
    try:
        import Engine.UnicornPy as UnicornPy
    except:
        import UnicornPy as UnicornPy

class UnicornBlackCheckSignal():

    def __init__(self):
        self.killcheck = multiprocessing.Event()
        self.nbchan = 8
        self.freqratio = []
        self.pointstd = []
        self.filtereddata = []
        self.psddata = []
        self.psdclean = []
        self.freqdata = []
        self._samplefreq = 250.0
        self.highpassfilter = 0.1
        self.lowpassfilter = 20
        self.notchfilter = 60
        self.scale = 500.0
        self.controlband = [20, 40]
        self.noiseband = [58, 62]
        self.data = []
        self.psdonfiltereddata = True

    def check(self):
        self.freqratio = []
        self.pointstd = []
        self.filtereddata = []
        self.psddata = []
        self.psdclean = []
        self.freqdata = []
        #start = time.perf_counter()
        for cP in range(self.nbchan):
            signalnoiseratio, signalvariability, datavector, _power, _freqs = UnicornGroomQuality(self.data[:,cP], self.controlband, self.noiseband, self._samplefreq, self.scale, self.highpassfilter, self.lowpassfilter, self.notchfilter)
            self.freqratio.append(signalnoiseratio)
            self.pointstd.append(signalvariability)
            self.filtereddata.append(datavector)
            self.psddata.append(_power)
            self.freqdata.append(_freqs)
            if self.psdonfiltereddata:
                _power, _freqs = UnicornGroomPSD(datavector, self._samplefreq, self.scale)
                self.psdclean.append(_power)
            #print('channel %d noise ratio: %0.2f' % (cP, signalnoiseratio))
            #print('channel %d variance: %0.2f' % (cP, signalvariability))
        #finish = time.perf_counter()
        #print(f'Finished in {round(finish-start,8)} seconds(s)')

def UnicornGroomPSD(datavector, samplefreq=250.0, scale=500):
    #print('UnicornGroomPSD: called')
    # function to obtain spectral power
    overlaplength = int(len(datavector)/3.0)
    if (int(scale) <= overlaplength):
        overlaplength = int(int(scale)/2.0)

    _power = []
    _freqs = []
    _power, _freqs = mlab.psd(x=datavector, NFFT=int(scale), Fs=samplefreq, noverlap=overlaplength, sides='onesided', scale_by_freq=True)
    return _power, _freqs
    #print('UnicornGroomPSD: complete')

def UnicornGroomFilter(datavector, highpassfilter=0.1, lowpassfilter=20.0, notchfilter=60.0, samplefreq=250.0):
    #print('UnicornGroomFilter: called')
    # function to return filtered data

    # Apply notch filter
    if not (float(notchfilter) == float(0.0)):
        b, a = scipy.signal.iirnotch(notchfilter, 30.0, samplefreq) # Design notch filter
        datavector = scipy.signal.filtfilt(b=b, a=a, x=datavector, padtype='constant', padlen=int(math.floor(len(datavector)/3.0)), method="pad")

    continuefilt = False
    if not (float(highpassfilter) == float(0.0)):
        if not (float(lowpassfilter) == float(0.0)):
            # band pass
            b, a = scipy.signal.iirfilter(3, [highpassfilter, lowpassfilter], btype='bandpass', ftype='butter', fs=samplefreq, output='ba')
            #sos = scipy.signal.iirfilter(3, [highpassfilter, lowpassfilter], btype='bandpass', ftype='butter', fs=samplefreq, output='sos')
            continuefilt = True
        else:
            # high pass
            b, a = scipy.signal.iirfilter(3, highpassfilter, btype='highpass', ftype='butter', fs=samplefreq, output='ba')
            #sos = scipy.signal.iirfilter(3, highpassfilter, btype='highpass', ftype='butter', fs=samplefreq, output='sos')
            continuefilt = True
    elif not (float(lowpassfilter) == float(0.0)):
        # low pass
        b, a = scipy.signal.iirfilter(3, lowpassfilter, btype='lowpass', ftype='butter', fs=samplefreq, output='ba')
        #sos = scipy.signal.iirfilter(3, lowpassfilter, btype='lowpass', ftype='butter', fs=samplefreq, output='sos')
        continuefilt = True

    if continuefilt:
        #datavector = scipy.signal.filtfilt(b=b, a=a, x=datavector, padtype='constant', padlen=int(math.floor(len(datavector)/3.0)), method="pad")
        datavector = scipy.signal.filtfilt(b=b, a=a, x=datavector, padtype=None)
        #datavector = scipy.signal.sosfilt(sos, datavector)

    return datavector
    #print('UnicornGroomFilter: complete')

def UnicornGroomQuality(datavector, controlband, noiseband, samplefreq, scale, highpassfilter, lowpassfilter, notchfilter):
    # function to evaluate quality of signal
    signalnoiseratio = []
    signalvariability = []
    datavector = numpy.ravel(datavector)
    _power, _freqs = UnicornGroomPSD(datavector, samplefreq=samplefreq, scale=scale)
    controlpower = numpy.median(_power[numpy.argmin(abs(_freqs-(controlband[0]))):numpy.argmin(abs(_freqs-(controlband[1])))])
    noisepower = numpy.mean(_power[numpy.argmin(abs(_freqs-(noiseband[0]))):numpy.argmin(abs(_freqs-(noiseband[1])))])

    if (float(controlpower) == float(0.0)):
        controlpower = 0.0001

    signalnoiseratio = numpy.around(numpy.divide(noisepower, controlpower), decimals=1) # frequency ratio

    datavector = UnicornGroomFilter(datavector, highpassfilter=highpassfilter, lowpassfilter=lowpassfilter, notchfilter=notchfilter, samplefreq=samplefreq)

    checkspan = int(math.floor((len(datavector) / 5.0))) * -1
    signalvariability = numpy.std(datavector[checkspan:-1])
    return signalnoiseratio, signalvariability, datavector, _power, _freqs

def UnicornJockey(deviceID, channellabels, rollingspan, logfilename, printoutput, startrecordingeeg, eegready, eegrecording, safetologevent, markeeg, markvalue, pulleegdata, conn, stoprecordingeeg):
    # this is the function that gets pushed to a seperate process that actually controls the device

    startedrecording = False

    # connect device
    UnicornBlack = UnicornBlackThreads()
    UnicornBlack.channellabels = channellabels # change channel labels
    UnicornBlack.printoutput = printoutput
    UnicornBlack.connect(deviceID=deviceID, rollingspan=rollingspan, logfilename=logfilename)
    eegready.set()

    continueroutine = True
    while continueroutine:

        if markeeg.is_set():
            UnicornBlack.mark_event(markvalue.value) # Send trigger
            markeeg.clear()

        if not startedrecording:
            if startrecordingeeg.is_set():
                UnicornBlack.startrecording() # rename file name here
                startedrecording = True
                eegrecording.set()

        if startedrecording:
            if stoprecordingeeg.is_set():
                UnicornBlack._safetolog = True # push any remaining items to file
                UnicornBlack.disconnect()
                continueroutine = False

        if safetologevent.is_set():
            UnicornBlack.safe_to_log(True)
        else:
            UnicornBlack.safe_to_log(False)

        if pulleegdata.is_set():
            sample = UnicornBlack.sample_data()
            #print('UnicornJockey: Battery at %0.1f percent' % numpy.array(sample)[-1,-3])
            if len(sample) > 0:
                conn.send(sample[:])
                pulleegdata.clear()

class UnicornBlackProcess():
    # this will be the class that the user interfaces with that intializes and maintains the multiprocessing

    def __init__(self):
        self.channellabels = 'FZ, C3, CZ, C4, PZ, O1, OZ, O2, AccelX, AccelY, AccelZ, GyroX, GyroY, GyroZ, Battery, Sample'
        self.samplefreq = 250.0
        self.numberOfAcquiredChannels = 17
        self.ready = False
        self.recording = False
        self.printoutput = False

    def connect(self, deviceID=None, rollingspan=3.0, logfilename='default'):
        # because of the multiprocessing, to not create additional headaches, the file name needs to be initiallized at connect

        self.deviceID = deviceID
        self.rollingspan = rollingspan
        self.logfilename = logfilename

        # connect to Device
        self.startrecordingeeg = multiprocessing.Event()
        self.stoprecordingeeg = multiprocessing.Event()
        self.eegready = multiprocessing.Event()
        self.eegrecording = multiprocessing.Event()
        self.safetologevent = multiprocessing.Event()
        self.markeeg = multiprocessing.Event()
        self.markvalue = multiprocessing.Value('i', 0)

        # code to be able to pull data
        self.pulleegdata = multiprocessing.Event()
        self.pulleegdata1, self.pulleegdata2 = multiprocessing.Pipe()

        self.p = multiprocessing.Process(target=UnicornJockey, args=[self.deviceID, self.channellabels, self.rollingspan, self.logfilename, self.printoutput, self.startrecordingeeg, self.eegready, self.eegrecording, self.safetologevent, self.markeeg, self.markvalue, self.pulleegdata, self.pulleegdata2, self.stoprecordingeeg])
        self.p.start()
        self.eegready.wait(3.0) # wait up to 3 seconds
        self.ready = True

    def startrecording(self):
        self.startrecordingeeg.set()
        self.eegrecording.wait(3.0) # wait up to 3 seconds
        self.recording = True

    def disconnect(self):
        self.stoprecordingeeg.set()
        self.p.join()
        self.pulleegdata1.close()

    def mark_event(self, event):
        self.markvalue.value = event
        self.markeeg.set()

    def safe_to_log(self, boolsafe=True):
        """Parameter to change logging settings
        """
        if boolsafe:
            self.safetologevent.set()
        else:
            self.safetologevent.clear()

    def sample_data(self):
        #t = time.perf_counter()
        self.pulleegdata.set() # tell process to obtain a sample
        data = self.pulleegdata1.recv() # takes about 20 ms
        #print(time.perf_counter() - t)
        #tempdata = numpy.array(data)
        #print('UnicornBlackProcess: Battery at %0.1f percent' % tempdata[-1,-3])
        return data

    def check_battery(self):
        _plottingdata = numpy.array(self.sample_data(), copy=True)
        powerlevel = 0
        try:
            powerlevel = int(_plottingdata[-1,-3]) # update power level
        except:
            powerlevel = 0
        if self.printoutput:
            print('Device battery at %d percent.' % powerlevel)
        return powerlevel

class UnicornBlackThreads():
    """class for data collection using the g.tec Unicorn Hybrid Black
	"""

    def __init__(self):

        # establish variables
        self.collectversion = '2020.05.30.0'
        self.device = None;
        self.path = os.path.dirname(os.getcwd())
        self.outputfolder = 'Raw'

        # create a Queue and Lock
        self._queue = Queue()
        self._queuelock = Lock()
        self._logsamplequeue = Queue()
        self._logeventqueue = Queue()
        self._bufferlock = Lock()
        self._loglock = Lock()
        self._logeventlock = Lock()

        # initialize data collectors
        self.logdata = False
        self.logfilename = None
        self._timetemp = None
        self._safetolog = False
        self._dataheaderlog = False

        # establish parameters
        self._configuration = None
        self._numberOfAcquiredChannels = None
        self._frameLength = 1
        self._samplefreq = 250.0
        self._intsampletime = 1.0 / self._samplefreq / 10
        self.channellabels = 'FZ, C3, CZ, C4, PZ, O1, OZ, O2, AccelX, AccelY, AccelZ, GyroX, GyroY, GyroZ, Battery, Sample'

        # activate
        self._receiveBuffer = None
        self.lastsampledpoint = None
        self.data = None
        self.printoutput = True
        self.ready = True
        self.deviceconnected = False

    def connect(self, deviceID=None, rollingspan=3.0, logfilename='default'):

        self.deviceID = deviceID;
        self.logfilename = logfilename
        # make sure everything is disconnected
        try:
            self.device.StopAcquisition()
        except:
            pass
        del self.device
        self.device = None
        # Did the user specify a particular device
        if (self.deviceID is None):
            try:
                # Get available device serials.
                deviceList = UnicornPy.GetAvailableDevices(True)

                if len(deviceList) <= 0 or deviceList is None:
                    raise Exception("No device available.Please pair with a Unicorn first.")

                # Print available device serials.
                print("Available devices:")
                i = 0
                for device in deviceList:
                    print("#%i %s" % (i,device))
                    i+=1

                # Request device selection.
                deviceID = int(input("Select device by ID #"))
                if deviceID < 0 or deviceID > len(deviceList):
                    raise IndexError('The selected device ID is not valid.')
                self.deviceID = deviceList[deviceID]

            except UnicornPy.DeviceException as e:
                print(e)
            except Exception as e:
                print("An unknown error occured. %s" %e)

        # Open selected device.
        try:
            self.deviceconnected = 0
            try:
                self.device = UnicornPy.Unicorn(self.deviceID)
                self.deviceconnected = 1
            except:
                pass

            if self.deviceconnected == 1:
                # Initialize acquisition members.
                self._rollingspan = rollingspan # seconds
                self._logchunksize = int(math.floor(5 * self._samplefreq))

                self._numberOfAcquiredChannels = self.device.GetNumberOfAcquiredChannels()
                self._configuration = self.device.GetConfiguration()

                # Allocate memory for the acquisition buffer.
                self._receiveBufferBufferLength = self._frameLength * self._numberOfAcquiredChannels * 4
                self._receiveBuffer = bytearray(self._receiveBufferBufferLength)
                self.data = [[0.0] * self._numberOfAcquiredChannels] * math.floor( float(self._rollingspan) * float(self._samplefreq) )

                try:
                    # initialize sample streamer
                    self._streaming = True
                    self._ssthread = Thread(target=self._stream_samples, args=[self._logsamplequeue], daemon=True)
                    self._ssthread.name = 'samplestreamer'
                except:
                    print("Error initializing sample streamer.")

                try:
                    # initialize data recorder
                    self._recording = True
                    self._drthread = Thread(target=self._log_sample, args=[self._logsamplequeue], daemon=True)
                    self._drthread.name = 'datarecorder'

                except:
                    print("Error initializing data recorder.")

                try:
                    # initialize event recorder
                    self._eventrecording = True
                    self._erthread = Thread(target=self._log_event, args=[self._logeventqueue], daemon=True)
                    self._erthread.name = 'eventrecorder'

                except:
                    print("Error initializing event recorder.")

                try:
                    # start processes
                    self.device.StartAcquisition(False)  # True - test signal; False - measurement mode
                except:
                    print("Error starting acquisition.")

                self.logdata = False
                self._ssthread.start()
                self._drthread.start()
                self._erthread.start()

                time.sleep(1) # give it some initialization time

                if self.printoutput:
                    print("Connected to '%s'." %self.deviceID)
            else:
                if self.printoutput:
                    print("Unable to connect to '%s'." %self.deviceID)
        except:
            if self.printoutput:
                print("Unable to connect to '%s'." %self.deviceID)

    def disconnect(self):

        time.sleep((self._intsampletime * float(10)))

        #self.stoprecording()
        self._streaming = False
        try:
            self._ssthread.join()
            self._drthread.join()
            self._erthread.join()
        except:
            pass

        try:
            self.device.StopAcquisition()
        except:
            pass

        try:
            self._logfile.close()
        except:
            pass

        del self.device
        self.device = None
        if self.printoutput:
            print("Disconnected from '%s'." %self.deviceID)

    def _stream_samples(self, queue):
        """Continuously polls the device, and puts all new samples in a
		Queue instance

		arguments

		queue		--	a multithreading.Queue instance, to put samples
						into
		"""

        # keep streaming until it is signalled that we should stop
        while self._streaming:
            boolgetdata = False
            self._bufferlock.acquire(True)
            try:
                #sampledata = [numpy.ndarray.tolist(numpy.multiply(numpy.random.rand(int(self._numberOfAcquiredChannels)), 100))]
                # Receives the configured number of samples from the Unicorn device and writes it to the acquisition buffer.
                self.device.GetData(self._frameLength,self._receiveBuffer,self._receiveBufferBufferLength)
                # Convert receive buffer to numpy float array
                sampledata = numpy.frombuffer(self._receiveBuffer, dtype=numpy.float32, count=self._numberOfAcquiredChannels * self._frameLength)
                sampledata = numpy.reshape(sampledata, (self._frameLength, self._numberOfAcquiredChannels))
                boolgetdata = True
            except:
                self._receiveBufferBufferLength = self._frameLength * self._numberOfAcquiredChannels * 4
                self._receiveBuffer = bytearray(self._receiveBufferBufferLength)
                if self.printoutput:
                    print('\n\nOverflow error in polling device.\n\n')
            self._bufferlock.release()

            if boolgetdata:
                self._queuelock.acquire(True)
                if not (str(int(float(self.data[-1][15]))) == str(int(float(sampledata[0][15])))): # protect against sampling the same point twice
                    #self.lastsampledpoint = copy.deepcopy(sampledata[0][15])
                    self._logeventlock.acquire(True)
                    self.lastsampledpoint = str(int(float(sampledata[0][15])))
                    self._logeventlock.release()
                    queue.put(sampledata)
                    self.data.append(sampledata[0])
                    self.data.pop(0)
                self._queuelock.release()

                self._bufferlock.acquire(True)
                self._receiveBuffer = bytearray(self._receiveBufferBufferLength) # make sure everything is cleared
                self._bufferlock.release()

            #time.sleep(float(0.000000000001))
            # if it is in, then we suddenly start getting blocking and drop samples
            # if we comment it out then we get all the samples, but run into an issue with screen flips

        self._queuelock.acquire(True)
        queue.put(None) # poison pill approach
        self._queuelock.release()
        self._eventrecording = False

    def _log_sample(self, logqueue):
        """Continuously log samples

		queue		--	a multithreading.Queue instance, to read samples
						from
		"""

        templogholding = None
        finalpush = False
        # keep trying to log until it is signalled that we should stop
        while self._recording:
            # read new item from the queue
            while not logqueue.empty():
                self._loglock.acquire(True)
                sampledata = logqueue.get()
                self._loglock.release()

                if sampledata is None:
                    # Poison pill means shutdown
                    self._recording = False
                    finalpush = True
                    break
                    break
                else:
                    if self.logdata:
                        if templogholding is None:
                            templogholding = sampledata[:,0:-1]
                        else:
                            templogholding = numpy.vstack([templogholding, sampledata[:,0:-1]])

                        # check if it is safe to log
                        if self._safetolog:
                            # only write chunks of data to save I/O overhead
                            if (len(templogholding) >= self._logchunksize):
                                numpy.savetxt(self._logfile,templogholding,delimiter=',',fmt='%.3f',newline='\n')
                                self._logfile.flush() # internal buffer to RAM
                                os.fsync(self._logfile.fileno()) # RAM file cache to disk
                                templogholding = None
        if finalpush:
            if templogholding is not None:
                numpy.savetxt(self._logfile,templogholding,delimiter=',',fmt='%.3f',newline='\n')
                self._logfile.flush() # internal buffer to RAM
                os.fsync(self._logfile.fileno()) # RAM file cache to disk
                templogholding = None
                self._logfile.close()

    def _log_event(self, logeventqueue):
        """Continuously log events

		logeventqueue	--	a multithreading.Queue instance, to read samples
						from
		"""
        eventheaderlog = False
        templogholding = None
        # keep trying to log until it is signalled that we should stop
        while self._eventrecording:
            # read new item from the queue
            while not logeventqueue.empty():
                self._logeventlock.acquire(True)
                sampledata = logeventqueue.get()
                self._logeventlock.release()

                # wait to create file until we know there is a need
                if self.logdata:
                    if not eventheaderlog:
                        header = 'collect.....= UnicornPy_' + self.collectversion + '\n'
                        header = header + 'date........= ' + self._timetemp  + '\n'
                        header = header + 'filename....= ' + self.logfilename  + '\n'
                        header = header + 'Latency, Event' + '\n'
                        self._eventlogfile = open('%s.csve' % (self.logfilename), 'w')
                        self._eventlogfile.write(header) # to internal buffer
                        self._eventlogfile.flush() # internal buffer to RAM
                        os.fsync(self._eventlogfile.fileno()) # RAM file cache to disk
                        eventheaderlog = True

                if sampledata is None:
                    # Poison pill means shutdown
                    self._eventrecording = False
                    break
                    break
                else:
                    if self.logdata:
                        if templogholding is None:
                            templogholding = sampledata
                        else:
                            templogholding = numpy.vstack([templogholding, sampledata])

                        # check if it is safe to log
                        if self._safetolog:
                            # only write chunks of data to save I/O overhead
                            if (len(templogholding) >= self._logchunksize):
                                numpy.savetxt(self._eventlogfile,templogholding,delimiter=',',fmt='%.3f',newline='\n')
                                self._eventlogfile.flush() # internal buffer to RAM
                                os.fsync(self._eventlogfile.fileno()) # RAM file cache to disk
                                templogholding = None

        if templogholding is not None:
                numpy.savetxt(self._eventlogfile,templogholding,delimiter=',',fmt='%s',newline='\n')
                self._eventlogfile.flush() # internal buffer to RAM
                os.fsync(self._eventlogfile.fileno()) # RAM file cache to disk
                templogholding = None
                self._eventlogfile.close()

    def startrecording(self):

        self.logdata = True
        timetemp = str(datetime.now()).split()
        self._timetemp = timetemp[0] + 'T' + timetemp[1]
        self._logfile = open('%s.csv' % (self.logfilename), 'w')
        self._safetolog = True
        self._log_header()

        # ensure we are getting data
        nc = 0
        while (str(int(float(self.data[-1][15]))) == str(int(float(0.0)))):
            nc = nc + 1

        if self.printoutput:
            print("Starting Recording")

    def _log_header(self):
        """Logs a header to the data file
        """
        if not self._dataheaderlog:
            header = 'collect.....= UnicornPy_' + self.collectversion + '\n'
            header = header + 'device......= ' + self.deviceID  + '\n'
            header = header + ('samplerate..= %.3f' % self._samplefreq)  + '\n'
            header = header + ('channels....= %d' % (self._numberOfAcquiredChannels - 1))  + '\n'
            header = header + 'date........= ' + self._timetemp  + '\n'
            header = header + 'filename....= ' + self.logfilename  + '\n'

            header = header + self.channellabels + '\n'
            #'FZ, C3, CZ, C4, PZ, O1, OZ, O2, AccelX, AccelY, AccelZ, GyroX, GyroY, GyroZ, Battery, Sample'
            self._logfile.write(header) # to internal buffer
            self._logfile.flush() # internal buffer to RAM
            os.fsync(self._logfile.fileno()) # RAM file cache to disk
            self._dataheaderlog = True

    def mark_event(self, event):
        """Logs data to the event file
        """
        if self.lastsampledpoint is not None:
            self._logeventlock.acquire(True)
            self._logeventqueue.put(numpy.array([str(self.lastsampledpoint), str(event)]))
            self._logeventlock.release()

    def safe_to_log(self, boolsafe=True):
        """Parameter to change logging settings
        """
        self._safetolog = boolsafe

    def sample_data(self):
        try:
            datasample = self.data[:]
        except:
            datasample = []
        return datasample

    def check_battery(self):
        _plottingdata = numpy.array(self.sample_data(), copy=True)
        powerlevel = 0
        try:
            powerlevel = int(_plottingdata[-1,-3]) # update power level
        except:
            powerlevel = 0

        if self.printoutput:
            print('Device battery at %d percent.' % powerlevel)
        return powerlevel

# # # # #
# DEBUG #
if __name__ == "__main__":

    duration = 5 # seconds
    example = 'Thread' # Process or Thread

    if example == 'Thread':
        # Example code for calling the Unicorn device as a process
        UnicornBlack = UnicornBlackProcess()
    else:
        # Example code for calling the Unicorn device as a thread
        UnicornBlack = UnicornBlackThreads()

    # all other functions and calls remain the same
    UnicornBlack.connect(deviceID='UN-2019.05.51', rollingspan=3, logfilename='recordeddata_thread')

    UnicornBlack.startrecording()

    for incrX in range(duration):
        time.sleep(1)
        UnicornBlack.safe_to_log(False)
        UnicornBlack.mark_event(incrX)
        print("Time Lapsed: %d second" % (incrX+1))
        UnicornBlack.safe_to_log(True)

    example = numpy.array(UnicornBlack.sample_data())
    print('Battery at %0.1f percent' % example[-1,-3])
    UnicornBlack.disconnect()

    # Plotting function to check for dropped samples
    #
    #selectsampleddata = []
    #for incrX in range(len(sampleddata)):
    #    if not (sampleddata[incrX][0] == float(0)):
    #        selectsampleddata.append(sampleddata[incrX])
    #sampleddata = numpy.array(selectsampleddata)
    #del selectsampleddata

    #if (sampleddata.shape[0]) > 2:
    #    x = numpy.arange(sampleddata[0][15],sampleddata[-1][15],1)
    #    y = numpy.array([0] * len(x))
    #    for incrX in range(len(sampleddata)):
    #        index_min = numpy.argmin(abs(x-(sampleddata[incrX][15])))
    #        #y[index_min] = sampleddata[incrX][15]
    #        y[index_min] = 1
    #    del index_min, incrX
    #    x = x * (1/ 250.0)
    #    plt.plot(x,y)
    #    print('The streamer had a total of %d dropped samples (%0.1f%%).' %(len(y)-numpy.count_nonzero(y), ((len(y)-numpy.count_nonzero(y))/len(y))*100))

    # show saved data
    #lis = []
    #with open("C:\\Studies\\Python Collect\\Gentask\\Raw\\recordeddata.csv", "rb") as f:
    #    for cnt, line in enumerate(f):
    #        lis.append(line.decode("utf-8").split(','))
    #f.close()
    #del cnt, line
    #lis = lis[7:-1]
    #x2 = numpy.arange(int(float(lis[0][15])),int(float(lis[-1][15])),1)
    #y2 = numpy.array([0] * len(x2))
    #for incrX in range(len(lis)):
    #    index_min = numpy.argmin(abs(x2-(int(float(lis[incrX][15])))))
    #    y2[index_min] = 1
    #del index_min, incrX

    # read in event markers
    #eventlis = []
    #with open("C:\\Studies\\Python Collect\\Gentask\\Raw\\recordeddata.csve", "rb") as f:
    #    for cnt, line in enumerate(f):
    #        eventlis.append(line.decode("utf-8").split(','))
    #f.close()
    #del cnt, line
    #eventlis = eventlis[4:-1]

    #y3 = numpy.array([0] * len(x2))
    #for incrX in range(len(eventlis)):
    #    index_min = numpy.argmin(abs(x2-(int(float(eventlis[incrX][0])))))
    #    y3[index_min] = 2
    #del index_min, incrX
    #
    # x2 = x2 * (1/ 250.0)
    #plt.plot(x2,y2)
    #plt.plot(x2,y3)
    #print('The recording had a total of %d dropped samples (%0.1f%%).' %(len(y2)-numpy.count_nonzero(y2), ((len(y2)-numpy.count_nonzero(y2))/len(y2))*100))

    # core.wait was responsible for the dropped samples!

# # # # #


pos = input()

row = int(pos[1])

# 문자 'a,b,c..'를 숫자로 변경하기
col = int(ord(pos[0])) - int(ord('a')) + 1

count = 0

# 나이트 이동 범위
nights = [(-2, 1), (-2, -1), (2, 1), (2, -1),
(1, -2), (-1, -2), (1, 2), (-1, 2)]

for n in nights:
    new_row = row + n[0]
    new_col = col + n[1]

    if new_row >=1 and new_row <=8 and new_col >=1 and new_col <= 8:
        count += 1

print(count)


from dataclasses import dataclass, field
from functools import lru_cache
from itertools import combinations
import math
import os
import random
import shutil
from typing import List

import numpy as np

from cube import Cube
from tile import Tile
from ck3map import CK3Map, IMPASSABLE, new_rgb, Terrain
import continent_gen

NUM_KINGDOM_HEXES = sum([sum(x) for x in continent_gen.KINGDOM_SIZE_LIST])
NUM_CENTER_HEXES = sum(continent_gen.CENTER_SIZE_LIST)
NUM_BORDER_HEXES = sum(continent_gen.BORDER_SIZE_LIST)

RELIGION_DICT = {'catholic': 'christianity', 'orthodox': 'christianity', 'islam': 'islam', 'tibet': 'buddhism'}

# Maybe should parse this out of the base game file instead? That'd make it update more easily.
HOLY_SITE_LIST = [
    "jerusalem", "rome", "cologne", "santiago", "kent", "segrada_familia", "constantinople", "alexandria", "antioch", "aksum", "napata",
    "suenik", "colonea", "beirut", "baghdad", "farz", "kerala", "armagh", "iona", "visoki", "ragusa", "esztergom", "mecca", "medina",
    "cordoba", "sinai", "tinmallal", "fes", "nadjaf", "damascus", "siffa", "kufa", "basra", "nizwa", "sijilmasa", "bahrein", "yamama",
    "sinjar", "baalbek", "lalish", "nishapur", "dashtestan", "zozan", "semien", "sufed", "gerizim", "ahvaz", "samarkand", "udabhanda",
    "toledo", "memphis", "bodh_gaya", "kusinagara", "sarnath", "sanchi", "ajanta", "sagya", "dagon", "pagan", "atamasthana",
    "amaravati", "varanasi", "ayodhya", "mathura", "haridwar", "kanchipuram", "ujjayini", "dwarka", "palitana", "shikharj",
    "ranakpur", "ellora", "sittannavasal", "itanagar", "pemako", "rima", "tezu", "kathmandu", "ilam", "garwhal", "jumla", "maowun",
    "ngawa", "chakla", "gyaitang", "rebgong", "xingqing", "yijinai", "alxa", "yazd", "nok_kundi", "takht-i-sangin",
    "takht-e_soleyman", "ushi-darena", "uppsala", "lejre", "paderborn", "zeeland", "ranaheim", "jorvik", "konugardr", "raivola",
    "hiiumaa", "akkel", "perm", "kiev", "novgorod", "barlad", "plock", "pokaini", "torun", "braslau", "rugen", "pest", "kerch",
    "olvia", "poszony", "sarysyn", "kara_khorum", "qayaliq", "tavan_bogd", "preslav", "athens", "carthage", "olympus", "sparta",
    "kabul", "multan", "bost", "khotan", "balkh", "lhasa", "purang", "tyumen", "surgut", "ob", "olkhon", "awkar", "jenne",
    "niani", "kukiya", "wadan", "daura", "garumele", "igbo", "el_fasher", "wandala", "kisi", "sherbro", "kayor", "kasa",
    "bono", "kumasi", "ife", "nikki", "aswan", "wadi_el_milk", "naqis", "sennar", "danakil", "kaffa", "harar", "makhir",
    "mogadishu", "gilgit", "suzhou"
]

def make_dot_mod(file_dir, mod_name, mod_disp_name):
    '''Build the basic mod details file.'''
    shared = "version = \"0.0.1\"\n"
    shared += "tags = {\n\t\"Total Conversion\"\n}\n"
    shared += "name = \"{}\"\n".format(mod_disp_name)
    shared += "supported_version = \"1.4.4\"\n"
    outer = "path = \"mod/{}\"\n".format(mod_name)

    replace_paths = [
        "common/bookmark_portraits", "common/culture/innovations", "common/dynasties",
        "history/characters", "history/cultures", "history/province_mappings", "history/provinces", "history/titles", "history/wars"
        ]
    shared += "replace_path = \"" + "\"\nreplace_path = \"".join(replace_paths)+"\""
    os.makedirs(os.path.join(file_dir, mod_name), exist_ok=True)
    with open(os.path.join(file_dir,"{}.mod".format(mod_name)),'w') as f:
        f.write(shared + outer)
    with open(os.path.join(file_dir, mod_name, "descriptor.mod".format(mod_name)),'w') as f:
        f.write(shared)

@dataclass
class MinorIsland:
    name: str = ""
    religion: str = ""

@dataclass
class Center:
    name: str = ""
    religion: str = ""

@dataclass
class Border:
    name: str = ""
    religion: str = ""

@dataclass
class Kingdom:
    name: str = ""
    island: bool = False
    religion: str = ""

@dataclass
class Empire:
    name: str = ""
    angle: int = 0
    kingdoms: List["Kingdom"] = field(default_factory=list)
    centers: List["Center"] = field(default_factory=list)
    borders: List["Border"] = field(default_factory=list)
    minor_islands: List["MinorIsland"] = field(default_factory=list)
    base_terrain: Terrain = Terrain.plains
    waste_terrain: Terrain = Terrain.plains
    continentals: int = 0
    islands: int = 0
    titles: List = field(default_factory=list)

    def title_order(self):
        '''Returns the names of files to write in the landed_titles, in order, from a list.'''
        if len(self.titles) > 0:
            return self.titles
        num_kingdoms = self.continentals
        if num_kingdoms == 0:  # special case for solo island continent
            return self.island_names()
        num_centers = num_kingdoms - 2
        num_borders = 2 * num_kingdoms - 3
        assert len(self.centers) >= num_centers, 'Not enough centers for {}'.format(self.name)
        assert len(self.borders) >= num_borders, 'Not enough borders for {}'.format(self.name)
        centers = [c.name for c in self.centers]
        random.shuffle(centers)
        centers = centers[:num_centers]
        borders = [c.name for c in self.borders]
        random.shuffle(borders)
        borders = borders[:num_borders]
        kingdoms = [k for k in self.continental_names()]
        island_kingdoms = [k for k in self.island_names()]
        minor_islands = [m.name for m in self.minor_islands]
        random.shuffle(kingdoms)
        random.shuffle(island_kingdoms)
        random.shuffle(minor_islands)
        titles = []
        titles.append(centers[0])
        titles.extend(kingdoms[:3])
        titles.extend(borders[:3])
        if num_kingdoms == 3:
            titles.extend(island_kingdoms)
            return titles
        titles.append(centers[1])
        titles.append(kingdoms[3])
        titles.extend(borders[3:5])
        if num_kingdoms == 4:
            titles.extend(island_kingdoms)
            return titles
        titles.append(centers[2])
        titles.append(kingdoms[4])
        titles.extend(borders[5:])
        titles.extend(island_kingdoms)
        titles.extend(minor_islands)
        return titles

    def add_kingdom(self, kingdom):
        self.kingdoms.append(kingdom)
        if kingdom.island:
            self.islands += 1
        else:
            self.continentals += 1

    def continental_names(self):
        return [k.name for k in self.kingdoms if not k.island]

    def island_names(self):
        return [k.name for k in self.kingdoms if k.island]

def read_config_file(config_filepath):
    '''Read the config file for the map.
    This ignores everything after a #, and expects rows to look like:
    empire_name kingdom_name    religion    continent/island
    separated by tabs.'''
    config_file = open(config_filepath)
    empires = {}
    base_terrain_from_empire = {}
    waste_terrain_from_empire = {}
    # kingdoms_from_religion = {}
    # others_from_religion = {}
    for line in config_file:
        split_line = [el.lower() for el in line.rstrip().split('#')[0].split('\t')]
        if len(split_line) == 2: #angle info
            empire, angle = split_line
            empires[empire] = Empire(name=empire, angle=int(angle))
        elif len(split_line) == 3: #terrain info
            empire, base_terrain, waste_terrain = split_line
            base_terrain_from_empire[empire] = Terrain[base_terrain]
            waste_terrain_from_empire[empire] = Terrain[waste_terrain]
            empires[empire].base_terrain = Terrain[base_terrain]
            empires[empire].waste_terrain = Terrain[waste_terrain]
        elif len(split_line) == 4: #kingdom info
            empire, kname, religion, geo_type = split_line
            assert geo_type == 'island' or geo_type == 'continent'
            if kname[0] == 'k':
                kingdom = Kingdom(kname, geo_type == 'island', religion)
                empires[empire].add_kingdom(kingdom)
                # if religion in kingdoms_from_religion:
                #     kingdoms_from_religion[religion].append(kingdom.name)
                # else:
                #     kingdoms_from_religion[religion] = [kingdom.name]
            elif kname[0] == 'c':
                center = Center(kname, religion)
                empires[empire].centers.append(center)
                # if religion in others_from_religion:
                #     others_from_religion[religion].append(center.name)
                # else:
                #     others_from_religion[religion] = [center.name]
            elif kname[0] == 'b':
                border = Border(kname, religion)
                empires[empire].borders.append(border)
                # if religion in others_from_religion:
                #     others_from_religion[religion].append(border.name)
                # else:
                #     others_from_religion[religion] = [border.name]
            elif kname[0] == 'm':
                minor_island = MinorIsland(kname, religion)
                empires[empire].minor_islands.append(minor_island)
                # if religion in others_from_religion:
                #     others_from_religion[religion].append(minor_island.name)
                # else:
                #     others_from_religion[religion] = [minor_island.name]
    return (empires, base_terrain_from_empire, waste_terrain_from_empire)

def find_ocean_and_wastelands(world):
    '''Given a tile, return a dictionary of distance from shore for the ocean and a list of list of hexes for each interior hole.'''
    bounding_hex = continent_gen.BoundingHex(world, extra = 1)
    non_land = [k for k, v in bounding_hex.dists.items() if v >= 1]
    non_land_chunks = continent_gen.get_chunks(non_land)
    max_index = -1
    max_len = 0
    for idx in range(len(non_land_chunks)):
        if len(non_land_chunks[idx]) > max_len:
            max_index = idx
            max_len = len(non_land_chunks[idx])
    ocean = non_land_chunks.pop(max_index)
    return {k: bounding_hex.dists[k] for k in ocean}, non_land_chunks

def calc_land_height(world, ocean):
    land_height = {}
    # boundary = world.boundary()  # This doesn't work because boundary counts water hexes.
    height = 0
    next_set = set()
    for el in world.real_hex_list():
        if any([nel in ocean for nel in el.neighbors()]):
            land_height[el] = height
            next_set.update([nel for nel in el.neighbors() if nel not in ocean])
    current = set(next_set)
    while len(current) > 0:
        height += 1
        next_set = set()
        for el in current:
            if el not in land_height:
                land_height[el] = height
            next_set.update([nel for nel in el.neighbors() if nel not in ocean and nel not in land_height])
        current = set(next_set)
    return land_height

def check_contiguous(group):
    '''Check to make sure that every cube in a list is reachable by walking through that list.'''
    if len(group) == 0:
        return True
    to_search = {group[0]}
    found = set()
    nfound = 0
    while len(to_search) > 0:
        curr = to_search.pop()
        nfound += 1
        found.add(curr)
        try:
            to_search.union([el for el in curr.neighbors() if el in group and el not in to_search and el not in found])
        except:
            continue
    return len(group) == nfound

def split_group(divide):
    '''Divide a group into two groups, both about half the size.'''
    target_low = math.floor(len(divide) * 0.5)
    target_high = math.ceil(len(divide) * 0.5)
    maxnn = 0
    for el in divide:
        nn = 0
        for nel in el.neighbors():
            if nel not in divide:
                nn += 1
        if nn > maxnn:
            maxnn = nn
            loc = el
    valid = False
    locs_to_try = random.sample(divide, len(divide))
    try_index = 0
    while not valid and try_index < len(divide):
        dist_to_loc = {}
        to_fill_el = [loc]
        to_fill_d = [0]
        while len(to_fill_el)>0:
            curr = to_fill_el.pop(0)
            dist = to_fill_d.pop(0)
            valid_neighbors = [el for el in curr.neighbors() if el not in dist_to_loc and el not in to_fill_el and el in divide]
            to_fill_el.extend(valid_neighbors)
            to_fill_d.extend([dist+1]*len(valid_neighbors))
            dist_to_loc[curr] = dist
        sorted_dtl = sorted(dist_to_loc.items(), key=lambda x: x[1])
        if sorted_dtl[target_low-1][1] < sorted_dtl[target_low][1]:
            a = [el[0] for el in sorted_dtl[:target_high]]
            b = [el[0] for el in sorted_dtl[target_high:]]
        else:
            median = sorted_dtl[target_low][1]
            a = [el[0] for el in sorted_dtl if el[1] < median]
            b = [el[0] for el in sorted_dtl if el[1] > median]
            m = [el[0] for el in sorted_dtl if el[1] == median]
            while len(m) > 0:
                na = []
                nb = []
                nm = []
                for el in m:
                    n = el.neighbors()
                    if len([el for el in n if el in a]) > len([el for el in n if el in b]):
                        na.append(el)
                    elif len([el for el in n if el in a]) < len([el for el in n if el in b]):
                        nb.append(el)
                    else:
                        nm.append(el)
                if len(na) + len(nb) == 0:
                    random.shuffle(m)
                    if len(a) < len(b): #Favor a if it has less elements, otherwise b.
                        a.extend(m[:math.ceil(len(m)*0.5)])
                        b.extend(m[math.ceil(len(m)*0.5):])
                    else:
                        b.extend(m[:math.ceil(len(m)*0.5)])
                        a.extend(m[math.ceil(len(m)*0.5):])
                    m = []
                else:
                    a.extend(na)
                    b.extend(nb)
                    m = nm
        #Check that a and b are contiguous.
        valid = check_contiguous(a) and check_contiguous(b) and len(a) > 0 and len(b) > 0
        if not valid:
            loc = locs_to_try[try_index]
            try_index += 1
    if not valid: #We tried all of the possible starting locations and couldn't get an even contiguous split.
        maxnn = 0
        for el in divide:
            nn = 0
            for nel in el.neighbors():
                if nel in divide:
                    nn += 1
            if nn > maxnn:
                maxnn = nn
                loc = el
        #Now we have the element with the most neighbors in the set, which means it's most likely the important boundary.
        #We can build subsets in a way that guarantees contiguity.
        sources = [el for el in loc.neighbors() if el in divide]
        groups = []
        while len(sources) > 0:
            source = sources.pop(0)
            if not any([source in group for group in groups]):
                groups.append([source])
                m = [el for el in source.neighbors() if el in divide and el != loc]
                while len(m) > 0:
                    curr = m.pop(0)
                    if curr not in groups[-1]:
                        groups[-1].append(curr)
                    m.extend([el for el in curr.neighbors() if el in divide and el not in groups[-1] and el != loc])
        max_len = max([len(group) for group in groups])
        groups = [group for group in groups if len(group) == max_len]
        a = random.sample(groups,1)[0]
        b = [el for el in divide if el not in a]
    return (a,b)

def group_seas(ocean, min_sea_size=3, max_sea_size=12):
    '''Given a dictionary of distance from shore, create a list of list of hexes that are shallow water,
    and a list of list of hexes that are deeper (but still traversable) water.'''
    water_groups = []
    shore = [k for k, v in ocean.items() if v == 1]
    shore_n = [len([nel for nel in el.neighbors() if nel in shore]) for el in shore]
    w_n = [len([nel for nel in el.neighbors() if nel in ocean]) for el in shore]
    eshore = [el for index, el in enumerate(shore) if shore_n[index] == w_n[index]]
    for el in eshore:
        if not any([el in wg for wg in water_groups]):
            water_groups.append([el])
            to_search = [h for h in el.neighbors() if h in eshore]
            while len(to_search) > 0:
                curr = to_search.pop()
                if curr not in water_groups[-1]:
                    water_groups[-1].append(curr)
                to_search.extend([h for h in curr.neighbors() if h in eshore and h not in water_groups[-1]])
    to_split = [wg for wg in water_groups if len(wg) > max_sea_size]
    while len(to_split) > 0:
        divide = to_split[0]
        a, b = split_group(divide)
        water_groups.remove(divide)
        water_groups.append(a)
        water_groups.append(b)
        to_split = [wg for wg in water_groups if len(wg) > max_sea_size]
    water_groups = [wg for wg in water_groups if len(wg) > 1]
    for wg in water_groups:
        if len(wg) <= min_sea_size:
            neighbors = []
            for el in wg:
                neighbors.extend([nel for nel in el.neighbors() if nel in shore and not any([nel in wag for wag in water_groups])])
            neighbors = list(set(neighbors))
            if len(wg) + len(neighbors) <= max_sea_size:
                wg.extend(neighbors)
    ends = [el for el in shore if not any([el in wag for wag in water_groups]) and
        sum([nel in shore and not any([nel in wag for wag in water_groups]) for nel in el.neighbors()]) == 1]
    while len(ends) > 0:
        for src in ends:
            if not any(src in wag for wag in water_groups):
                water_groups.append([src])
                valid = True
                while valid:
                    neighbors = []
                    for el in water_groups[-1]:
                        neighbors.extend([nel for nel in el.neighbors() if nel in shore and not any([nel in wag for wag in water_groups])])
                    if len(neighbors) == 0:
                        valid = False
                        if len(water_groups[-1]) == 1: #We should just glom on to a neighbor.
                            bn_size = 0
                            bn_ind = 0
                            for index, wg in enumerate(water_groups):
                                n_size = sum([nel in wg for nel in src.neighbors()])
                                if n_size > bn_size:
                                    bn_ind = index
                            water_groups[bn_ind].append(src)
                            water_groups.pop()
                    elif len(water_groups[-1]) + len(neighbors) < max_sea_size:
                        water_groups[-1].extend(neighbors)
                    else:
                        valid = False
        ends = [el for el in shore if not any([el in wag for wag in water_groups]) and
            sum([nel in shore and not any([nel in wag for wag in water_groups]) for nel in el.neighbors()]) == 1]
        lonely_shore = [el for el in shore if not any([el in wag for wag in water_groups])]
        if len(ends) == 0 and len(lonely_shore) > 0:
            ends = random.sample(lonely_shore,1)
    depths = [el for el, h in ocean.items() if h == 1]
    seas = []
    for el in depths:
        nels = el.neighbors()
        if all([nel in ocean and ocean[nel] == 0 for nel in nels]):
            most_adj = 0
            for wg in water_groups:
                adj = sum([nel in wg for nel in nels])
                if adj > 0 and len(wg) == 2:
                    best = wg
                    most_adj = 7
                elif adj > most_adj:
                    best = wg
                    most_adj = adj
            best.append(el)
        elif all([nel in ocean and ocean[nel] < 2 for nel in nels]):
            seas.append(el)
    sea_groups = []
    for el in seas:
        if not any(el in sg for sg in sea_groups):
            sea_groups.append([el])
            to_search = [h for h in el.neighbors() if h in seas]
            while len(to_search) > 0:
                curr = to_search.pop()
                if curr not in sea_groups[-1]:
                    sea_groups[-1].append(curr)
                to_search.extend([h for h in curr.neighbors() if h in seas and h not in sea_groups[-1]])
    for sg in sea_groups:
        if len(sg) <= 2:
            for el in sg:
                most_adj = 0
                nels = el.neighbors()
                for wg in water_groups:
                    adj = sum([nel in wg for nel in nels])
                    if adj > 0 and len(wg) == 2:
                        best = wg
                        most_adj = 7
                    elif adj > most_adj:
                        best = wg
                        most_adj = adj
                best.append(el)
    sea_groups = [sg for sg in sea_groups if len(sg) > 2]
    if len(sea_groups) > 0:
        while max([len(el) for el in sea_groups]) > 5:
            to_split = [sg for sg in sea_groups if len(sg) > 5]
            divide = to_split[0]
            a, b = split_group(divide)
            sea_groups.remove(divide)
            sea_groups.append(a)
            sea_groups.append(b)
    return water_groups, sea_groups

def allocate_pids(world, wastelands, shore_groups, sea_groups):
    '''Assigns a pid and RGB to each hex.'''
    pid_from_hex = {}
    rgb_from_hex = {}
    rgb_from_pid = {}
    last_pid = 1
    for el in world.real_hex_list():
        rgb = new_rgb(rgb_from_pid)
        rgb_from_hex[el] = rgb
        pid_from_hex[el] = last_pid
        rgb_from_pid[last_pid] = rgb
        last_pid += 1
    for group in wastelands:
        rgb = new_rgb(rgb_from_pid)
        rgb_from_pid[last_pid] = rgb
        for el in group:
            rgb_from_hex[el] = rgb
            pid_from_hex[el] = last_pid
        last_pid += 1
    for group in shore_groups + sea_groups:
        rgb = new_rgb(rgb_from_pid)
        rgb_from_pid[last_pid] = rgb
        for el in group:
            rgb_from_hex[el] = rgb
            pid_from_hex[el] = last_pid
        last_pid += 1
    return pid_from_hex, rgb_from_hex, rgb_from_pid, last_pid

def make_terrain(world, wastelands, ocean, empires, base_terrain_from_empire, waste_terrain_from_empire):
    '''Create the dictionary that maps cubes to terrain.
    The main terrain used is:
        plains: the default, present everywhere
        farmland: rare, present everywhere, biased towards capitals.
        mountains: rare, present everywhere
        hills: uncommon, present everywhere

    There are four regional variants for base terrain:
        forest (western Europe)
        steppe (eastern Europe)
        jungle (india)
        desert (islam)
    '''
    terrain_from_hex = {}
    flattened_kingdom_hexes = [item for sublist in continent_gen.KINGDOM_SIZE_LIST for item in sublist]
    for emp_idx, empire in enumerate(empires.keys()):
        base_terrain = base_terrain_from_empire[empire]
        waste_terrain = waste_terrain_from_empire[empire]
        for tile in world.tile_list[emp_idx].tile_list:
            tile_hexes = tile.real_hex_list()
            if len(tile_hexes) == NUM_KINGDOM_HEXES:
                # There are 16 counties: 1 capital farmland, 6 plains, 4 hills, 3 mountains, and 2 base.
                terrain_list = [Terrain.plains] * 6 + [Terrain.hills] * 4 + [Terrain.mountains] * 3 + [base_terrain] * 2
                random.shuffle(terrain_list)
                terrain_list.insert(0, Terrain.farmlands)
                terrain_list = [[terrain_type] * num_hexes for terrain_type, num_hexes in zip(terrain_list, flattened_kingdom_hexes)]
            elif len(tile_hexes) == NUM_CENTER_HEXES:
                # There are 4 counties: farmland capital, 1 plains, 1 hills, and 1 base.
                terrain_list = [Terrain.plains] * 1 + [Terrain.hills] * 1 + [base_terrain] * 1
                random.shuffle(terrain_list)
                terrain_list.insert(0, Terrain.farmlands)
                terrain_list = [[terrain_type] * num_hexes for terrain_type, num_hexes in zip(terrain_list, continent_gen.CENTER_SIZE_LIST)]
            elif len(tile_hexes) == NUM_BORDER_HEXES:
                # There are 3 counties: 1 base, 1 hills, and 1 mountains.
                terrain_list = [base_terrain] * 1 + [Terrain.hills] * 1 + [Terrain.mountains] * 1
                random.shuffle(terrain_list)
                terrain_list = [[terrain_type] * num_hexes for terrain_type, num_hexes in zip(terrain_list, continent_gen.BORDER_SIZE_LIST)]
            else:
                terrain_list = [[base_terrain]] * len(tile_hexes)
                # raise ValueError('Don\'t know how to handle terrain for this tile: {} {}'.format(emp_idx, tile))
            # This was a list of lists, where each barony in a county just shares the same terrain, and we want it to be a flattened list to match the real_hex_list.
            terrain_list = [item for sublist in terrain_list for item in sublist]
            for el in tile_hexes:
                terrain_from_hex[el] = terrain_list.pop(0)
            bounding_hex = continent_gen.BoundingHex(world.tile_list[emp_idx])
            for wasteland in wastelands:
                if wasteland[0] in bounding_hex:
                    for el in wasteland:
                        terrain_from_hex[el] = waste_terrain
    return terrain_from_hex

def make_province_terrain_txt(terrain_from_hex, pid_from_hex, file_dir):
    '''Create the common/00_province_terrain.txt file.'''
    os.makedirs(os.path.join(file_dir,"common", "province_terrain"), exist_ok=True)
    with open(os.path.join(file_dir,"common", "province_terrain", "00_province_terrain.txt"),'w', encoding='utf8') as f:
        f.write('default=plains\n')
        buffer = []
        for cube_loc, terrain_type in terrain_from_hex.items():
            buffer.append((pid_from_hex[cube_loc], terrain_type.name))
        buffer = sorted(buffer)
        f.write("\n".join([str(a) + "=" + b for a, b in buffer]) + "\n")
    with open(os.path.join(file_dir,"common", "province_terrain", "01_province_properties.txt"),'w', encoding='utf8') as f:
        # This is where winter severity would go.
        f.write("\n")

def make_landed_titles(empires, religions, src_dir, out_dir):
    '''Create the common/landed_titles/00_landed_titles.txt (and map_data/geographical_region). Also generates the bname_from_pid dict.'''
    os.makedirs(os.path.join(out_dir,"common", "landed_titles"), exist_ok=True)
    os.makedirs(os.path.join(out_dir,"history", "provinces"), exist_ok=True)
    bname_from_pid = {}
    bname = "ERROR"
    pid=1
    with open(os.path.join(out_dir, "map_data", "island_region.txt"), "w", encoding='utf8') as isl_region_file:
        # TODO: Make this real.
        isl_region_file.write("#No islands generated.\n")
    with open(os.path.join(out_dir, "common", "landed_titles", "00_landed_titles.txt"),'w', encoding='utf8') as outf:
        with open(os.path.join(out_dir, "map_data", "geographical_region.txt"), "w", encoding='utf8') as geo_region_file:
            # Initial constants.
            with open(os.path.join(src_dir, "start", "common", "landed_titles", "landed_titles.txt"), encoding='utf8') as inf:
                outf.write(inf.read())
            outf.write('\n')
            # Go through each religion to write out the special titles.
            for religion in religions:
                with open(os.path.join(src_dir, "r_" + religion, "common", "landed_titles", "landed_titles.txt"), encoding='utf8') as inf:
                    outf.write(inf.read())
                outf.write('\n')
            outf.write('\n')
            # Go through each empire, and then each kingdom in each empire.
            for ename, titles in empires.items():
                with open(os.path.join(src_dir, "e_" + ename, "common", "landed_titles", "landed_titles.txt"), encoding='utf8') as inf:
                    # If you want to edit the empire capital, this is where to do it.
                    outf.write(inf.read())
                outf.write('\n')
                geo_region_duchy_list = []
                geo_region_kingdom_list = []
                kingdom_duchy_list = []
                # Sadly, the title_order is the order in which the hexes are placed (and thus the pids are calculated),
                # but it's not the order in which the titles are written, because we need to lump all of the duchies
                # together under a dummy kingdom. So the duchies go first, and then the kingdoms.
                kingdom_buffer = ""
                for title in titles:
                    barony_mapping = {}
                    flip_rival = 0
                    if title[0] == 'k':
                        # For kingdoms, we want to construct a geographical region in the georegion file.
                        # TODO: This is where we should check for island kingdoms and add them to island_region instead.
                        region_name = f"world_{ename}_{title}"
                        kingdom_duchy_list = []
                        geo_region_kingdom_list.append(region_name)
                    with open(os.path.join(src_dir, title, "common", "landed_titles", "landed_titles.txt"), encoding='utf8') as inf:
                        for line in inf.readlines():
                            if line.startswith("\t\td_"):
                                flip_rival += 1
                                if title[0] == "k":
                                    kingdom_duchy_list.append(line.split("=")[0].strip())
                                else:
                                    geo_region_duchy_list.append(line.split("=")[0].strip())
                            if "\t\tb_" in line:
                                bname = line.split('\t\tb_')[1].split('=')[0].strip()
                            if "province = P" in line:
                                front, back = line.split("= P")
                                if title[0] == "k" and flip_rival == 2:
                                    barony_mapping["P" + back.split()[0]] = str(pid + 14)
                                    line = front + "= " + str(pid + 14) + "\n"
                                    bname_from_pid[pid + 14] = bname
                                elif title[0] == "k" and flip_rival == 3:
                                    barony_mapping["P" + back.split()[0]] = str(pid - 14)
                                    line = front + "= " + str(pid - 14) + "\n"
                                    bname_from_pid[pid - 14] = bname
                                else:
                                    barony_mapping["P" + back.split()[0]] = str(pid)
                                    line = front + "= " + str(pid) + "\n"
                                    bname_from_pid[pid] = bname
                                pid += 1
                            if title[0] == 'k':
                                kingdom_buffer += line
                            else:
                                outf.write(line)
                    if title[0] == 'k':
                        # Write the kingdom's geographical region.
                        duchies = " ".join(kingdom_duchy_list)
                        geo_region_file.write(f"{region_name} = {{\n\tduchies = {{\n\t\t{duchies}\n\t}}\n}}\n")
                        kingdom_buffer += "\n"
                    else:
                        outf.write("\n")
                    # Write out history/provinces file.
                    with open(os.path.join(src_dir, title, "history", "provinces", title + ".txt"), encoding='utf8') as inf:
                        with open(os.path.join(out_dir, "history", "provinces", title + ".txt"), 'w', encoding='utf8') as hpoutf:
                            for line in inf.readlines():
                                if line[0] == 'P':
                                    sloc = line.index(" ")
                                    try:
                                        line = barony_mapping[line[:sloc]] + line[sloc:]
                                    except:
                                        raise(Exception(f"{line} {sloc} not found in barony_mapping (len({len(barony_mapping)})), {title}."))
                                hpoutf.write(line)

                outf.write("\t}\n") # End the dummy kingdom for all of the duchies.
                outf.write(kingdom_buffer)
                outf.write('\n}\n')
                # Write the middle geographical region.
                middle_region_name = f"world_{ename}_middle"
                duchies = " ".join(geo_region_duchy_list)
                geo_region_file.write(f"{middle_region_name} = {{\n\tduchies = {{\n\t\t{duchies}\t}}\n}}\n")
                # Write the empire's geographical region.
                regions = " ".join(geo_region_kingdom_list + [middle_region_name])
                geo_region_file.write(f"world_{ename} = {{\n\tregions = {{\n\t\t{regions}\t}}\n}}\n")
    return bname_from_pid

def make(file_dir = 'C:\\ck3_procedural\\', mod_name='testing_modgen', mod_disp_name='SinglePlayerTest',
         config_filepath = 'C:\\ck3_procedural\\config.txt',
         max_x=1280, max_y=1280, num_rivers = 25, crisp = True, seed = None):
    '''Attempts to make the folder with mod.'''
    #Basic Setup.
    mod_dir = os.path.join(file_dir, mod_name)
    data_dir = os.path.join(file_dir, "data")
    if os.path.exists(mod_dir):
        shutil.rmtree(mod_dir)
    os.makedirs(mod_dir, exist_ok=True)
    os.makedirs(os.path.join(mod_dir,"map_data"), exist_ok=True)
    os.makedirs(os.path.join(mod_dir,'common'), exist_ok=True)
    make_dot_mod(file_dir, mod_name, mod_disp_name)
    if seed:
        random.seed(seed)
    #Read in the configuration files.
    empires, base_terrain_from_empire, waste_terrain_from_empire = read_config_file(config_filepath)
    e_titles = {k: v.title_order() for k, v in empires.items()}
    religions = make_religions(e_titles, "c_roma", data_dir, mod_dir)  # TODO: update the default hs_county to be read from the list of real counties.
    assert len(empires) == 3
    cont_size_list = [v.continentals for v in empires.values()]
    island_size_list = [v.islands for v in empires.values()]
    assert(sum(island_size_list)) == 0, "Haven't implemented all the island stuff yet."
    angles = [v.angle for v in empires.values()]
    #Generate the tile hierarchy.
    world = continent_gen.make_world(cont_size_list=cont_size_list, island_size_list=island_size_list, angles=angles)  # , inner_sea=continent_gen.CENTER_SIZE_LIST)
    world.rectify()
    max_mag = max([el.mag() for el in world.real_hex_list()])
    #Fill in wastelands and group together the seas.
    ocean, wastelands = find_ocean_and_wastelands(world)
    shore_groups, sea_groups = group_seas(ocean)
    #Create hex to province id mapping and hex to RGB mapping.
    pid_from_hex, rgb_from_hex, rgb_from_pid, last_pid = allocate_pids(world, wastelands, shore_groups, sea_groups)
    cmap = CK3Map(max_x, max_y, hex_size = 24, map_size = max_mag + 2, crisp=crisp, default = IMPASSABLE)
    cmap.d_cube2rgb = rgb_from_hex
    cmap.d_cube2pid = pid_from_hex
    cmap.provinces(filedir=mod_dir)
    land_height = calc_land_height(world, ocean)
    cmap.d_cube2terr = make_terrain(world, wastelands, ocean, e_titles, base_terrain_from_empire, waste_terrain_from_empire)
    make_province_terrain_txt(cmap.d_cube2terr, pid_from_hex, mod_dir)
    waste_list = [*wastelands]
    bname_from_pid = make_landed_titles(e_titles, religions, data_dir, mod_dir)
    # Make a bunch of map_data files. Maybe this should be split out to a separate function?
    make_adjacencies(mod_dir, world, pid_from_hex, bname_from_pid, cmap)
    make_climate(mod_dir)
    make_definition(mod_dir, rgb_from_pid, bname_from_pid)
    cmap.heightmap(land_height, ocean, waste_list, filedir=mod_dir)
    major_rivers, last_pid = cmap.rivers(land_height, bname_from_pid, num_rivers, last_pid, mod_dir)
    for category in [["events"]] + [[os.path.join("gfx","map","map_object_data")]] + [[os.path.join("history", "cultures")]] + [["common", x] for x in ["casus_belli_types", "decisions", "flavorization", "on_action", "story_cycles"]]:
        copy_over(os.path.join(data_dir, "start"), mod_dir, so_far=category)
    cmap.positions(bname_from_pid, ocean, filedir=mod_dir)
    make_dynasties(e_titles, religions, data_dir, mod_dir)
    make_default(mod_dir, waste_list, pid_from_hex, major_rivers, last_pid)
    make_bookmark(data_dir, mod_dir)
    make_innovations(data_dir, mod_dir)
    return world, cmap, ocean, land_height, waste_list, bname_from_pid, last_pid

def make_adjacencies(file_dir, world, pid_from_hex, bname_from_pid, cmap):
    with open(os.path.join(file_dir,"map_data", "adjacencies.csv"),'w', encoding='utf8') as f:
        f.write('From;To;Type;Through;start_x;start_y;stop_x;stop_y;Comment\n')
        # Check for Gibraltar-like adjacencies.
        for (tile_a, tile_b) in combinations(world.tile_list, 2):
            strait_pairs, corner_pairs = tile_a.two_step_pairs(tile_b)
            # Exclude any corner pairs that are next to a strait pair, which should be used instead.
            corner_pairs = [c for c in corner_pairs if not(any([c[0] in s[0].neighbors() or c[1] in s[1].neighbors() for s in strait_pairs]))]
            for (hex_a, hex_b) in strait_pairs:
                # This is a connection between the two vertices of a strait, calculated from two trios of hexes.
                # TODO: Probably this logic should live in the ck3map class, and just return the x,y pairs?
                pid_a = pid_from_hex[hex_a]
                pid_b = pid_from_hex[hex_b]
                trios = hex_a.foursome(hex_b)
                x_a, y_a = cmap.find_vertex(trios[0])
                x_b, y_b = cmap.find_vertex(trios[1])
                comment = f"{bname_from_pid.get(pid_a)}-{bname_from_pid.get(pid_b)}"
                # From;To;Type;Through;start_x;start_y;stop_x;stop_y;Comment
                # TODO: Add through pid!
                f.write(";".join([str(s) for s in [pid_a, pid_b, "sea", x_a, y_a, x_b, y_b, comment]]) + "\n")
            for (hex_a, hex_b) in corner_pairs:
                # This is a connection between the two edges separated by one water hex.
                pid_a = pid_from_hex[hex_a]
                pid_b = pid_from_hex[hex_b]
                middle = hex_a.avg(hex_b)
                x_a, y_a = cmap.edge_middle(hex_a, middle)
                x_b, y_b = cmap.edge_middle(hex_b, middle)
                comment = f"{bname_from_pid.get(pid_a)}-{bname_from_pid.get(pid_b)}"
                # From;To;Type;Through;start_x;start_y;stop_x;stop_y;Comment
                f.write(";".join([str(s) for s in [pid_a, pid_b, "sea", x_a, y_a, x_b, y_b, comment]]) + "\n")
        # TODO: Add adjacency for island kingdoms.

        # This line is now written later, because maybe you added major river adjacencies.
        # f.write('-1;-1;;-1;-1;-1;-1;-1;')

def make_bookmark(src_dir, out_dir):
    '''Makes the bookmarks.'''
    # TODO: Make this actually dynamic.
    os.makedirs(os.path.join(out_dir,"common", "bookmarks"), exist_ok=True)
    with open(os.path.join(out_dir,"common", "bookmarks", "00_bookmarks.txt"),'w', encoding='utf8') as outf:
        outf.write("mp_game = {\n")
        outf.write("\tstart_date = 1000.1.1\n")
        outf.write("\tis_playable = yes\n\n")
        outf.write("\tcharacter = {\n")
        outf.write("\t\tname = \"Hugues\"\n")
        outf.write("\t\tdynasty = 440\n")
        outf.write("\t\tdynasty_splendor_level = 1\n")
        outf.write("\t\ttype = male\n")
        outf.write("\t\tbirth = 980.11.26\n")
        outf.write("\t\ttitle = k_france\n")
        outf.write("\t\tgovernment = feudal_government\n")
        outf.write("\t\tculture = french\n")
        outf.write("\t\treligion = catholic\n")
        outf.write("\t\tdifficulty = \"BOOKMARK_CHARACTER_DIFFICULTY_EASY\"\n")
        outf.write("\t\thistory_id = 2202\n")
        outf.write("\t\tposition = { 765 590 }\n\n")
        outf.write("\t\tanimation = disapproval\n")
        outf.write("\t}\n}\n")
    os.makedirs(os.path.join(out_dir, "common", "bookmark_portraits"), exist_ok=True)
    with open(os.path.join(out_dir, "common", "bookmark_portraits", "bookmark_test_character.txt"),'w', encoding='utf8') as outf:
        with open(os.path.join(src_dir, "start", "common", "bookmark_portraits", "bookmark_test_character.txt"), encoding='utf8') as inf:
            outf.write(inf.read())

def make_climate(file_dir):
    with open(os.path.join(file_dir,"map_data", "climate.txt"),'w', encoding='utf8') as f:
        # TODO: This is currently just a placeholder climate file (borrowed from https://forum.paradoxplaza.com/forum/threads/resource-nearly-blank-map-a-starting-point-for-tcs-v1-2.1414703/page-5#post-27133344 ).
        f.write('mild_winter = {\n\t1 2 3 4 5 6 7 8\n}\n')

def make_default(file_dir, waste_list, pid_from_hex, major_rivers, last_pid):
    with open(os.path.join(file_dir,"map_data", "default.map"),'w', encoding='utf8') as f:
        f.write("definitions = \"definition.csv\"\n")
        f.write("provinces = \"provinces.png\"\n")
        f.write("rivers = \"rivers.png\"\n")
        f.write("topology = \"heightmap.heightmap\"\n")
        f.write("continent = \"continent.txt\"\n")
        f.write("adjacencies = \"adjacencies.csv\"\n")
        f.write("island_region = \"island_region.txt\"\n")
        f.write("geographical_region = \"geographical_region.txt\"\n")
        f.write("seasons = \"seasons.txt\"\n")
        f.write("\n#############\n# SEA ZONES\n#############\n\n")
        # Note this groups all of the sea zones together; maybe we should split them up a bit.
        if len(waste_list) > 0:
            first_sea = pid_from_hex[waste_list[-1][-1]] + 1
        else:
            raise NotImplementedError
        if len(major_rivers) > 0:
            last_sea = major_rivers[0] - 1
        else:
            last_sea = last_pid - 1
        f.write(f"sea_zones = RANGE {{ {first_sea} {last_sea} }}")

        f.write("\n###############\n# MAJOR RIVERS\n###############\n\n")
        # TODO: Currently this doesn't try to lump them together at all, but it should.
        for major_river in major_rivers:
            f.write(f"river_provinces = LIST {{ {major_river} }}\n")

        f.write("\n########\n# LAKES\n########\n\n")
        # TODO: Add lakes. We currently don't generate any, but it might be a replacement for some wastelands.

        f.write("\n#####################\n# IMPASSABLE TERRAIN\n#####################\n\n")
        # Provinces that can be colored by ownership; basically all of the ones that we'll make.
        for waste in waste_list:
            # TODO Maybe check to see if it should be a range instead of a list if possible? Like this should be the same order.
            f.write("impassable_mountains = LIST { " + str(pid_from_hex[waste[0]]) + " }\n")

        f.write("\n############\n# WASTELAND\n############\n\n")
        # Provinces that can't be colored by ownership. Probably won't be used, except for seas.
        f.write("# IMPASSABLE SEA ZONES\n")
        f.write(f"impassable_seas = LIST {{ {last_pid} }}")  # TODO add inner sea if it's not 0

def make_definition(file_dir, rgb_from_pid, name_from_pid):
    with open(os.path.join(file_dir,"map_data", "definition.csv"),'w', encoding='utf8') as f:
        f.write('0;0;0;0;x;x;\n')
        for pid, rgb in rgb_from_pid.items():
            if pid in name_from_pid:
                name = name_from_pid[pid]
            else:
                name = str(pid)
            f.write(';'.join([str(pid)] + [str(c) for c in rgb] + [name, 'x', '\n']))
        # f.write(f'{max(rgb_from_pid.keys()) + 1};255;255;255;x;x;\n') # I think I fixed the bug that needed this as a fix.

def copy_file(src_dir, title, area, file_name, out_dir):
    inf_loc = os.path.join(src_dir, title, area, file_name)
    if os.path.exists(inf_loc):
        with open(os.path.join(out_dir, area, file_name), 'w', encoding='utf8') as outf:
            with open(inf_loc, encoding='utf8') as inf:
                outf.write(inf.read())

def make_dynasties(empires, religions, src_dir, out_dir):
    '''Writes out the common/dynasties, history/characters, and history/titles files.'''
    os.makedirs(os.path.join(out_dir,"common", "dynasty_houses"), exist_ok=True)
    with open(os.path.join(out_dir,"common", "dynasty_houses", "00_dynasty_houses.txt"),'w', encoding='utf8') as outf:
        outf.write("")
    os.makedirs(os.path.join(out_dir,"history", "wars"), exist_ok=True)
    with open(os.path.join(out_dir,"history", "wars", "00_wars.txt"),'w', encoding='utf8') as outf:
        outf.write("")
    os.makedirs(os.path.join(out_dir,"history", "province_mappings"), exist_ok=True)
    with open(os.path.join(out_dir,"history", "province_mappings", "00_provinces.txt"),'w', encoding='utf8') as outf:
        outf.write("")
    os.makedirs(os.path.join(out_dir,"history", "characters"), exist_ok=True)
    os.makedirs(os.path.join(out_dir,"history", "titles"), exist_ok=True)
    os.makedirs(os.path.join(out_dir,"common", "coat_of_arms", "coat_of_arms"), exist_ok=True)
    os.makedirs(os.path.join(out_dir,"common", "dynasties"), exist_ok=True)
    areas = [os.path.join(*area_list) for area_list in [
        ["history", "characters"],
        ["history", "titles"],
        ["common", "coat_of_arms", "coat_of_arms"],
    ]]
    # Perhaps we should also replace the big 00_dynasties.txt? Or reconfigure coat_of_arms to do the same thing.
    with open(os.path.join(out_dir,"common", "dynasties", "00_dynasties.txt"),'w', encoding='utf8') as doutf:
        for religion in religions:
            for area in areas:
                copy_file(src_dir, f"r_{religion}", area, f"r_{religion}.txt", out_dir)
            inf_loc = os.path.join(src_dir, f"r_{religion}", "common", "dynasties", "00_dynasties.txt")
            if os.path.exists(inf_loc):
                with open(inf_loc, encoding='utf8') as inf:
                    doutf.write(inf.read())
        for titles in empires.values():
            for title in titles:
                for area in areas:
                    copy_file(src_dir, title, area, f"{title}.txt", out_dir)
                inf_loc = os.path.join(src_dir, title, "common", "dynasties", "00_dynasties.txt")
                if os.path.exists(inf_loc):
                    with open(inf_loc, encoding='utf8') as inf:
                        doutf.write(inf.read())

def make_innovations(src_dir, out_dir):
    '''Writes out the common/culture/innovations files.'''
    area = os.path.join("common", "culture", "innovations")
    os.makedirs(os.path.join(out_dir, area), exist_ok=True)
    for era in os.listdir(os.path.join(src_dir, "start", area)):
        with open(os.path.join(out_dir, area, era), 'w', encoding='utf8') as outf:
            with open(os.path.join(src_dir, "start", area, era), encoding='utf8') as inf:
                outf.write(inf.read())

def make_religions(empires, hs_county, src_dir, out_dir):
    '''Gather the relevant data and then write out the religion files.'''
    # First we get all of the holy sites from all of the titles.
    hs_dict = {}
    used_hs = set()
    religions = set()
    os.makedirs(os.path.join(out_dir, "common", "religion", "holy_sites"), exist_ok=True)
    with open(os.path.join(out_dir, "common", "religion", "holy_sites", "00_holy_sites.txt"), 'w', encoding='utf8') as outf:
        for titles in empires.values():
            for title in titles:
                hs_path = os.path.join(src_dir, title, "common", "religion", "holy_sites")
                if os.path.exists(hs_path):
                    for religion_file in os.listdir(hs_path):
                        hs_names = []
                        with open(os.path.join(hs_path, religion_file), encoding='utf8') as inf:
                            for line in inf.readlines():
                                if line[0] not in ['\t', '}', '#'] and '=' in line:
                                    if line[0] == '\ufeff':
                                        line = line[1:]
                                    hs_names.append(line.split('=')[0].strip())
                                    used_hs.add(hs_names[-1])
                                outf.write(line)
                            outf.write("\n")
                        religion = religion_file.split('.')[0]
                        religions.add(RELIGION_DICT[religion])
                        if religion in hs_dict:
                            hs_dict[religion].extend(hs_names)
                        else:
                            hs_dict[religion] = hs_names
        # Now we write out the unused holy sites, which we blank out so that we can use the vanilla religions.
        for holy_site in HOLY_SITE_LIST:
            if holy_site not in used_hs:
                outf.write(f"{holy_site} = {{\n\tcounty = {hs_county}\n}}\n")
    # Now we know which religion files to write out, and what holy sites to give them.
    os.makedirs(os.path.join(out_dir, "common", "religion", "religions"), exist_ok=True)
    for religion in religions:
        with open(os.path.join(out_dir, "common", "religion", "religions", f"00_{religion}.txt"), 'w', encoding='utf8') as outf:
            with open(os.path.join(src_dir, f"r_{religion}", "common", "religion", "religions", f"00_{religion}.txt"), encoding='utf8') as inf:
                for line in inf.readlines():
                    if 'holy_site = ' in line:
                        hs_group = line.split('=')[1].strip()
                        for hs in hs_dict.get(hs_group, []):
                            outf.write(f"\t\t\tholy_site = {hs}\n")
                    else:
                        outf.write(line)
    return religions

def copy_over(src_dir, out_dir, so_far=[]):
    '''Copy over files that have been modified to drop inappropriate events / references / etc.'''
    for top_level in os.listdir(os.path.join(src_dir, *so_far)):
        if os.path.isdir(os.path.join(src_dir, *so_far, top_level)):
            copy_over(src_dir, out_dir, so_far=so_far + [top_level])
        else:
            this_dir = os.path.join(out_dir, *so_far)
            os.makedirs(this_dir, exist_ok=True)
            with open(os.path.join(this_dir, top_level), 'w', encoding='utf8') as outf:
                with open(os.path.join(src_dir, *so_far, top_level), encoding='utf8') as inf:
                    outf.write(inf.read())

if __name__ == '__main__':
    #Eventually we're going to use a config file to run most of this.
    make()


x = {}
type(x)

file_counts = {"jpg":10,"txt":14,"csv":2,"py":23}
print(file_counts)
print(file_counts["txt"])
print("jpg" in file_counts)
print("html" in file_counts)

file_counts["cfg"]=8
print(file_counts)

file_counts["csv"]=17
print(file_counts)

del file_counts["cfg"]
print(file_counts)

file_counts = {"jpg":[10,20],"txt":14,"csv":2,"py":23}
for extension in file_counts:
    print(extension)

for ext, amount in file_counts.items():
    print("There are {} file with the .{} extension".format(amount,ext))

print(file_counts.keys()) # dict_keys(['jpg','txt','csv','py'])
print(file_counts.values())

for value in file_counts.values():
    print(value)

cool_beasts = {"octopuses":"tentacles", "dolphins":"fins", "rhinos":"horns"}
for animal,parts in cool_beasts.items():
    print("{} have {}".format(animal,parts))

def count_letters(text):
    result={}# create curly bruckets
    for letter in text: # eg
        if letter not in result:
            result[letter]=0 #eg if a != a end of the count
        result[letter] +=1 # else continue cound the letter
    return result

print(count_letters("aaaaa"))
print(count_letters("tenant)"))
print(count_letters("a long string with a lot of letters"))

ip_addresses = ["192.168.1.1","127.0.0.1","8.8.8.8"]
host_address = {"router": "192.168.1.1","localhost":"127.0.0.1","google":"8.8.8.8"}

x = {"Practice1":10,"Practice5":30}
print(len(x))
for ket in x:
    print(ket)

for ket, valeu in x.items():
    print(ket,valeu)

print(x.get("Practice1"))
print(x.keys())
print(x.values())
y = {"Practice3":55,"Practice6":66}
print(x.update(y))
print(x.keys())
print(x.clear())
print(x.keys())

wardrobe = {'shirt': ['red', 'blue', 'white'], 'jeans': ['blue', 'black']}
new_items = {'jeans': ['white'], 'scarf': ['yellow'], 'socks': ['black', 'brown']}
wardrobe.update(new_items)
print(wardrobe)

def format_address(address_string):
  # Declare variables
    lista =''
    address=[]
  # Separate the address string into parts
    address_string = address_string.split()
  # Traverse through the address parts
    for parts in address_string:
        if parts.isnumeric():
            lista=parts
        else:
            address.append(parts)

    # Determine if the address part is the
    # house number or part of the street name

  # Does anything else need to be done
  # before returning the result?

  # Return the formatted string

    return "house number {} on street named {}".format(lista,' '.join(address))
print(format_address("123 Main Street"))
# Should print: "house number 123 on street named Main Street"

print(format_address("1001 1st Ave"))
# Should print: "house number 1001 on street named 1st Ave"

print(format_address("55 North Center Drive"))
# Should print "house number 55 on street named North Center Drive"

def highlight_word(sentence, word):
	result = []
	sentence = sentence.split()
	for cword in sentence:
		if word in cword:
			result.append(cword.upper())
		else:
			result.append(cword)
	return ' '.join(result)# convert list to string with blank

print(highlight_word("Have a nice day", "nice"))
print(highlight_word("Shhh, don't be so loud!", "loud"))
print(highlight_word("Automating with Python is fun", "fun"))

def combine_lists(list1, list2):
  new_list = []
  # Generate a new list containing the elements of list2
  # Followed by the elements of list1 in reverse order
  list1.reverse()
  new_list = list2+list1
  return new_list

Jamies_list = ["Alice", "Cindy", "Bobby", "Jan", "Peter"]
Drews_list = ["Mike", "Carol", "Greg", "Marcia"]

print(combine_lists(Jamies_list, Drews_list))

def squares(start, end):
	return [i**2 for i in range(start,end+1)]

print(squares(2, 3)) # Should be [4, 9]
print(squares(1, 5)) # Should be [1, 4, 9, 16, 25]
print(squares(0, 10)) # Should be [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

def car_listing(car_prices):
  result = ""
  for car, price in car_prices.items():
    result += "{} costs {} dollars".format(car,price) + "\n"
  return result

print(car_listing({"Kia Soul":19000, "Lamborghini Diablo":55000, "Ford Fiesta":13000, "Toyota Prius":24000}))

def combine_guests(guests1, guests2):
  new_list = []
  new_list = guests2
  new_list.update(guests1)

  return new_list
  # Combine both dictionaries into one, with each key listed
  # only once, and the value from guests1 taking precedence

Rorys_guests = { "Adam":2, "Brenda":3, "David":1, "Jose":3, "Charlotte":2, "Terry":1, "Robert":4}
Taylors_guests = { "David":4, "Nancy":1, "Robert":2, "Adam":1, "Samantha":3, "Chris":5}

print(combine_guests(Rorys_guests, Taylors_guests))

def count_letters(text):
  result = {}
  text= text.lower()
  print(text)
  # Go through each letter in the text
  for letter in text.replace(" ",""):
    # print(letter)
    # Check if the letter needs to be counted or not
    if letter.isalpha():

      count = text.count(letter)
      result[letter] = count
  return result

    # Add or increment the value in the dictionary
    # ___
  # return result

print(count_letters("AaBbCc"))
# Should be {'a': 2, 'b': 2, 'c': 2}

print(count_letters("Math is fun! 2+2=4"))
# Should be {'m': 1, 'a': 1, 't': 1, 'h': 1, 'i': 1, 's': 1, 'f': 1, 'u': 1, 'n': 1}

print(count_letters("This is a sentence."))
# Should be {'t': 2, 'h': 1, 'i': 2, 's': 3, 'a': 1, 'e': 3, 'n': 2, 'c': 1}

animal = "Hippopotamus"
print(animal[3:6])
print(animal[-5])
print(animal[10:])

colors = ["red", "white", "blue"]
colors.insert(2, "yellow")

host_addresses = {"router": "192.168.1.1", "localhost": "127.0.0.1", "google": "8.8.8.8"}
host_addresses.keys()


import sqlite3

if __name__ == '__main__':
    conn = sqlite3.connect("androidBug.db")
    cursor = conn.cursor()
    tb_exists = "SELECT name FROM sqlite_master WHERE type='table' AND name='bug_table'"
    if not conn.execute(tb_exists).fetchone():
        conn.execute(
            'create table bug_table (id  integer PRIMARY KEY autoincrement, url varchar(100),result varchar(100),description varchar(100),state varchar(20),value integer)')
        conn.commit()
    while True:
        url = input('url:')
        result = input('结果：')
        des = input('描述：')
        state = input('状态：')
        value = input('评价')
        sql = ''' insert into bug_table
              (url,result,description,state,value)
              values
              (:st_url,:st_result,:st_desc,:st_state,:st_value)'''
        cursor.execute(sql,
                       {'st_url': url, 'st_result': result, 'st_desc': des, 'st_state': state, 'st_value': value})
        conn.commit()
        print("=====================================")


"""Condition support for Local Sites."""

class LocalSiteModelChoiceMixin(object):
    """Mixin to restrict model choices to those on a Local Site.

    This will ensure that any queries are bound to a
    :py:class:`~reviewboard.site.models.LocalSite`, if provided to the
    condition field widget through
    :py:attr:`~djblets.forms.widgets.ConditionsWidget.choice_kwargs`.
    """

    #: The field on the model pointing to a LocalSite.
    local_site_attr = 'local_site'

    def get_queryset(self):
        """Return a queryset for the choice.

        The queryset will take into account a LocalSite, if one is provided
        for the conditions.

        Returns:
            django.db.models.query.QuerySet:
            The queryset for the choice.
        """
        queryset = super(LocalSiteModelChoiceMixin, self).get_queryset()
        local_site = self.extra_state.get('local_site')

        if local_site:
            queryset = queryset.filter(**{
                self.local_site_attr: local_site,
            })

        return queryset


from _dbConnector import *
from _api import *
import requests
import json
import time
import pika
import environ
from _utils_discord import create_discord_payload

env = environ.Env()
environ.Env.read_env()

# any(isinstance(e, int) and e > 0 for e in [1,2,'joe'])
# all(isinstance(e, int) and e > 0 for e in [1,2,'joe'])

def custom_reject(ch, method, body, reason=""):
    new_properties = pika.BasicProperties(
        headers={'x-rejection-reason': f"Reject bot {method.routing_key}, reason : {reason}"}
    )

    ch.basic_publish(
        exchange='',
        routing_key='message.dlq',
        properties=new_properties,
        body=body
    )

    ch.basic_ack(delivery_tag = method.delivery_tag)
    # ch.basic_reject(delivery_tag = method.delivery_tag, requeue=False)

def webhook_consumer(ch, method, properties, body, reject_first=False):
    from _utils_mylogger import mylogger, LOGGER_DEBUG, LOGGER_INFO, LOGGER_WARNING, LOGGER_ERROR

    if (reject_first == True):
        custom_reject(ch, method, body, reason="Reject first")
        return

    # ch.basic_ack(delivery_tag = method.delivery_tag)
    # ch.basic_reject(delivery_tag = method.delivery_tag, requeue=False)

    for i in range(4):
        try:
            content = json.loads(body)

            # discord_channel = content["channel"]
            discord_channel = method.routing_key.split('.')[0]

            webhook_url = None

            for key in env.ENVIRON:
                if key.startswith("BOT_WEBHOOK_"):
                    webhook_chan = key.replace("BOT_WEBHOOK_", "").lower()

                    if webhook_chan == discord_channel:
                        webhook_url = env(key)

            if (webhook_url == None):
                raise Exception(f'{discord_channel} channel webhook not found')

            payload = create_discord_payload(content)

            headers = {
                'Content-Type': 'application/json'
            }

            response = requests.post(webhook_url, data=json.dumps(payload), headers=headers)

            if response.status_code != 204:
                raise Exception(f'Channel {discord_channel}, status {response.status_code}, Response: {response.text}')

            mylogger(f"Consume webhook {method.routing_key}, chan {discord_channel}", LOGGER_INFO, rabbit=False)
            time.sleep(1.2)
            ch.basic_ack(delivery_tag = method.delivery_tag)
            break

        except Exception as e:
            if ("errors" not in method.routing_key):
                mylogger(f"Reject webhook {method.routing_key}, type {type(e)}, reason {e}", LOGGER_ERROR)

            if ('429' in str(e)):
                mylogger(f"Retry {i} webhook {method.routing_key}, type {type(e)}, reason {e}", LOGGER_INFO)
                time.sleep(1)
                continue

            custom_reject(ch, method, body, reason=f"type {type(e)}, exception {e}")
            break

    return True


from django.urls import path, re_path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("search", views.search, name="search"),
    path("new", views.new, name="new"),
    # path("new/<str:page>/", views.new, name="edits"),
    # path("edit/<str:entry>/", views.edit, name="edit"),
    re_path(r'edit/(?P<entry>[\w\ ]+)', views.edit, name="edit"),
    path('random', views.random, name='random'),
    path("<str:title>", views.article, name="article"),
]


"""
    This is the code to accompany the Lesson 2 (SVM) mini-project.

    Use a SVM to identify emails from the Enron corpus by their authors:
    Sara has label 0
    Chris has label 1
"""

import sys
from time import time
import pickle

sys.path.append(r"../tools/")
from email_preprocess import preprocess
from sklearn import svm
from sklearn.metrics import accuracy_score

# features_train and features_test are the features for the training
# and testing datasets, respectively
# labels_train and labels_test are the corresponding item labels
features_train, features_test, labels_train, labels_test = preprocess()

# features_train = features_train[:int(len(features_train) / 100)]
# labels_train = labels_train[:int(len(labels_train) / 100)]

#########################################################
try:
    with open('model.pickle', 'rb') as file:
        clf = pickle.load(file)
        print('Opening Saved Model, so no training time!!')
        print('Saved Model Training Time: 194 s')
except:
    clf = svm.SVC(kernel='linear')
    t0 = time()
    clf.fit(features_train, labels_train)
    print("Training time:", round(time() - t0, 3), "s")
    with open('model.pickle', 'wb') as file:
        pickle.dump(clf, file)

prediction = clf.predict(features_test)
accuracy = accuracy_score(labels_test, prediction)
print("Model Accuracy:", accuracy)

#########################################################


if __name__ == "__main__":
  import sys
  import os
  sys.path.insert(0,os.path.abspath("../../../"))

from bmark.bmarks.common import *
from lang.prog import MathProg
from ops import op, opparse
import bmark.menvs as menvs
from bmark.bmarks.other.bbsys import build_std_bb_sys
from bmark.bmarks.other.closed_form \
  import build_sin, build_cos, build_sin_and_cos

def model():
  params = {
    'meas_noise':0.3,
    'proc_noise':0.99999,
    'W0':0.707,
    'P0':0.1,
    'one':0.9999
  }

  prob = MathProg("kalman-freq-small")
  params['Rinv'] = 1.0/params['proc_noise']
  params['nRinv'] = -params['Rinv']
  params['Q'] = params['meas_noise']

  build_cos(prob,"0.5",0.5,"BB")
  build_sin_and_cos(prob, \
                    "WSQ","W", \
                    3.0, \
                    "COSW","WSINW",
                    normalize=False)

  WSq = parse_fn("W*W", params)
  SINW2 = parse_fn("WSINW*WSINW",params)

  dW = parse_diffeq('{nRinv}*P*WSINW*({one}*BB-{one}*COSW)', \
                    "W0",
                    ":w",
                    params)

  #{Q} + {nRinv}*WSINW2*P*P"
  dP = parse_diffeq("{Q}+{nRinv}*WSINW2*P*P", \
                    "P0", \
                    ":p", \
                    params)

  prob.bind("W",dW)
  prob.bind("P",dP)
  prob.bind("WSQ",WSq)
  prob.bind("WSINW2",SINW2)

  prob.set_interval("W",-10,10)
  prob.set_interval("P",-0.3,0.3)

  measure_var(prob,"WSQ","FREQ")
  prob.set_max_sim_time(200)
  prob.compile()
  menv = menvs.get_math_env('t200')
  return menv,prob

def execute():
  menv,prob = model()
  plot_diffeq(menv,prob)

def execute():
  menv,prob = model()
  plot_diffeq(menv,prob)

if __name__ == "__main__":
  execute()


import fileinput
import json
from itertools import permutations
from math import ceil

def main():
    inputs  = (x.rstrip() for x in fileinput.input())
    numbers = map(parse, inputs)
    result  = reduce(add, numbers)

    print(magnitude(result))
    print(max(magnitude(add(a, b)) for a, b in permutations(numbers, 2)))

def parse(s):
    num   = []
    depth = -1
    for c in s:
        if c == '[':
            depth += 1
        elif c == ']':
            depth -= 1
        elif c.isdigit():
            num.append((int(c), depth))

    return num

def add(a, b):
    num = []
    num.extend([(n, d + 1) for n, d in a])
    num.extend([(n, d + 1) for n, d in b])

    return solve(num)

def solve(num):
    while True:
        num, exploded = explode(num)
        if exploded:
            continue

        num, splitted = split(num)
        if splitted:
            continue

        break

    return num

def explode(num, depth=4):
    for i, (n, d) in enumerate(num):
        if d != depth:
            continue

        # To the left:
        if i > 0:
            num[i - 1] = (num[i - 1][0] + n, num[i - 1][1])

        # And to the right:
        if i + 2 < len(num):
            num[i + 2] = (num[i + 2][0] + num[i + 1][0], num[i + 2][1])

        # Replace with 0:
        num[i:i + 2] = [(0, d - 1)]

        return num, True

    return num, False

def split(num):
    for i, (n, d) in enumerate(num):
        if n > 9:
            num[i:i + 1] = [(n // 2, d + 1), (int(ceil(n / 2.0)), d + 1)]
            return num, True

    return num, False

def magnitude(num, depth=4):
    for i in reversed(range(0, depth)):
        reduced = True
        while reduced:
            num, reduced = magnitude_reduce(num, i)

    return num[0][0]

def magnitude_reduce(num, depth):
    for i, (n, d) in enumerate(num):
        if d != depth:
            continue

        num[i:i + 2] = [(3 * n + 2 * num[i + 1][0], d - 1)]

        return num, True

    return num, False

if __name__ == '__main__':
    main()

assert magnitude(parse('[[1,2],[[3,4],5]]')) == 143
assert magnitude(parse('[[[[0,7],4],[[7,8],[6,0]]],[8,1]]')) == 1384
assert magnitude(parse('[[[[1,1],[2,2]],[3,3]],[4,4]]')) == 445
assert magnitude(parse('[[[[3,0],[5,3]],[4,4]],[5,5]]')) == 791
assert magnitude(parse('[[[[5,0],[7,4]],[5,5]],[6,6]]')) == 1137
assert magnitude(parse('[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]')) == 3488