# Paralellism
import concurrent.futures

# CNN
from PIL import Image
import torch
import torchvision.transforms as T
import torch.nn as nn

# Utils
import xml.etree.ElementTree as ET
from datetime import datetime
import helpers
import cv2
import sys
import ast
import os

# Db
import mysql.connector

# Testing
import time


# Alexnet CNN Architecture
class mAlexNet(nn.Module):
def __init__(self, num_classes=2):
super(mAlexNet, self).__init__()
self.input_channel = 3
self.num_output = num_classes
self.layer1 = nn.Sequential(
nn.Conv2d(in_channels=self.input_channel, out_channels=16, kernel_size=11, stride=4),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2)
)
self.layer2 = nn.Sequential(
nn.Conv2d(in_channels=16, out_channels=20, kernel_size=5, stride=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2)
)
self.layer3 = nn.Sequential(
nn.Conv2d(in_channels=20, out_channels=30, kernel_size=3, stride=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2)
)
self.layer4 = nn.Sequential(
nn.Linear(30 * 3 * 3, out_features=48),
nn.ReLU(inplace=True)
)
self.layer5 = nn.Sequential(
nn.Linear(in_features=48, out_features=2)
)

def forward(self, x):
x = self.layer3(self.layer2(self.layer1(x)))
x = x.view(x.size(0), -1)
x = self.layer5(self.layer4(x))
m = nn.Softmax(dim=1)
x = m(x)
return x


# Deletes images from target folder
def clean_folder(folder_path, file_extension):
images_in_folder = helpers.get_file_names(folder_path, file_extension)
for i, image_path in enumerate(images_in_folder):
os.remove(image_path)


# Set db connection
def dbConnect():
try:
conn = mysql.connector.connect(
host=db_host,
database=db_dbame,
user=db_user,
password=db_password)
return conn

# Slave except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Create tables if needed
def check_db():
try:
# Create tb_zona
conn = dbConnect()
cur = conn.cursor()

query = f'CREATE TABLE IF NOT EXISTS tb_zona' \
f'(' \
f'id_zona INT AUTO_INCREMENT, ' \
f'zona VARCHAR(50) NOT NULL, ' \
f'referencia VARCHAR(100) NOT NULL, ' \
f'disponibilidad INT NOT NULL, ' \
f'img TEXT, ' \
f'parqueadero_e INT, ' \
f'estado INT,' \
f'PRIMARY KEY (id_zona)' \
f')'

cur.execute(query)
cur.close()
conn.commit()

# Create tb_parqueadero
conn = dbConnect()
cur = conn.cursor()
query = f'CREATE TABLE IF NOT EXISTS tb_parqueadero' \
f'(' \
f'id_parqueadero INT AUTO_INCREMENT, ' \
f'disponibilidad VARCHAR(2) NOT NULL, ' \
f'id_zona INT, ' \
f'numero INT, ' \
f'estado INT, ' \
f'PRIMARY KEY (id_parqueadero), ' \
f'FOREIGN KEY (id_zona)' \
f'REFERENCES tb_zona (id_zona)' \
f'ON UPDATE NO ACTION ' \
f'ON DELETE CASCADE' \
f') '

cur.execute(query)
cur.close()
conn.commit()

# Master except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Check zone
def check_zone(zone_name):
try:
# Set Connection
conn = dbConnect()
cur = conn.cursor()
query = f'SELECT id_zona FROM tb_zona WHERE zona = \'{zone_name}\';'
cur.execute(query)

if cur.fetchone() is None:
cur.close()

# Insert new zone
cur = conn.cursor()
query = f'INSERT INTO tb_zona(zona, referencia, parqueadero_e, img, disponibilidad, estado) ' \
f'VALUES (\'{zone_name}\', \'referencia nueva zona\', 1, \'img_text nueva zona\', 1, 1);'

cur.execute(query)
cur.close()
conn.commit()

conn.close()

# Master except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Check spot
def check_spot(zone_name, spot_number):
try:
# Get zone id
conn = dbConnect()
cur = conn.cursor()
query = f'SELECT id_zona FROM tb_zona WHERE zona = \'{zone_name}\';'
cur.execute(query)
id_zone = cur.fetchone()[0]
cur.close()

# Check if spot exists in zone
cur = conn.cursor()
query = f'SELECT id_parqueadero FROM tb_parqueadero WHERE id_zona = \'{id_zone}\' and numero = {spot_number};'
cur.execute(query)

if cur.fetchone() is None:
cur.close()

# Insert new spot
cur = conn.cursor()
query = f'INSERT INTO tb_parqueadero(disponibilidad, id_zona, numero, estado) ' \
f'VALUES (1, {id_zone}, {spot_number}, 1);'

cur.execute(query)
cur.close()
conn.commit()

else:
cur.close()

conn.close()

# Master except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Create some
def check_db_spots(dict_zone):
try:
for i, zone in enumerate(dict_zone):
check_zone(zone)
for j, spot in enumerate(dict_zone[zone]):
check_spot(zone, spot[4])

# Master except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Update Spot availability
def updateAvailability(spot_num, id_zone, is_available):
try:
# Set Connection
conn = dbConnect()

# Set Cursor
cur = conn.cursor()

# Sql UPDATE query
query = f'UPDATE tb_parqueadero ' \
f'SET disponibilidad = {is_available} ' \
f'WHERE id_zona = {id_zone} AND numero = {spot_num};'

# Execute SQL
cur.execute(query)

# Commit the changes to the database
conn.commit()

# Close communication with the PostgreSQL database
cur.close()

conn.close()

# Slave except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Check if spot is available
def process_zone(img_path):
try:
# Duplicated for paralellism
true_list = ['true', '1', 't', 'y', 'yes']

# Check storage neede
almacenar_lugares = get_params('almacenar_lugares', 0).text.lower()
root_folder = helpers.clear_folder_path(get_params('root_folder', 0).text)

store_image = False
if almacenar_lugares in true_list:
avalable_spots_path = f'{root_folder}/Spots_images/Available'
occupied_spots_path = f'{root_folder}/Spots_images/Occupied'
store_image = True

# Open original image
img_ori = cv2.imread(img_path)

# Get image name
img_name = (img_path.split('\\')[-1])[:-5]

# Get zone id
conn = dbConnect()
cur = conn.cursor()
query = f'SELECT id_zona FROM tb_zona WHERE zona = \'{img_name}\';'
cur.execute(query)
id_zone = cur.fetchone()[0]
cur.close()
conn.close()

# Get img spots coordinates
img_coordinates = parking_zones[img_name]

for i, region in enumerate(img_coordinates):

# Generate image copy
img = img_ori.copy()

# Crop current spot
cropped_img = img[region[0]:region[0] + region[1], region[2]:region[2] + region[3]]

# Crop name
crop_name = f'spot_{str(region[4])}_{img_name}'

# Store Crop
cv2.imwrite(f'{root_folder}/Zones_images/Temp_spots/{crop_name}.jpeg', cropped_img)

# Open spot
img = Image.open(f'{root_folder}/Zones_images/Temp_spots/{crop_name}.jpeg')

# Preprocess spot
preprocessed_img = preprocess(img)

# Extract tensor values
img_test = preprocessed_img.unsqueeze(0)

# Predict spot
pred = model(img_test)

# Extract predictions
predicted_class = pred.data.numpy()

# Get rounded prediction
y_pred = round(predicted_class[0][1], 2)

now = datetime.now()
if y_pred >= 0.75:
if store_image:
# Store Crop
cv2.imwrite(f'{occupied_spots_path}/{crop_name}_'
f'{now.strftime("%m_%d_%Y_%H_%M_%S")}.jpeg', cropped_img)
updateAvailability(region[4], id_zone, 0)
else:
if store_image:
cv2.imwrite(f'{avalable_spots_path}/{crop_name}_'
f'{now.strftime("%m_%d_%Y_%H_%M_%S")}.jpeg', cropped_img)
updateAvailability(region[4], id_zone, 1)

# Slave except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Retrieve params from xml
def get_params(param, index=None):
try:
# XML path
xml_path = f'./config_deteccion_de_disponibilidad.xml'
doc = ET.parse(xml_path)
root = doc.getroot()

if index is None:
return root.findall(param)
else:
return root.findall(param)[index]
# Slave except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
raise Exception(f'[{exc_tb.tb_lineno}] = {error}')


# Load critical parms (4 multiprocessing threads)
try:
# Get app params
procesamiento_paralelo = get_params('procesamiento_paralelo', 0).text.lower()
almacenar_lugares = get_params('almacenar_lugares', 0).text.lower()
root_folder = helpers.clear_folder_path(get_params('root_folder', 0).text)
weights_file_name = get_params('nombre_archivo_pesos', 0).text
file_extension = get_params('extension_imagenes', 0).text
tiempo_espera = get_params('tiempo_espera', 0).text
hora_cierre = get_params('hora_autocerrado', 0).text
emails = get_params("./emails", 0)

# Get DB params
db_password = get_params('contrasena_base', 0).text
db_dbame = get_params('nombre_base', 0).text
db_user = get_params('usuario_base', 0).text
db_host = get_params('host_base', 0).text


# Read Parking zones from xml
parking_zones = {}
for i, zone in enumerate(get_params('zona_parqueo')):
spots_list = []
for j, spot in enumerate(zone):
array = ast.literal_eval(spot.text)
if len(array) == 5:
spots_list.append(array)
if spots_list:
parking_zones[zone.get('name')] = spots_list

# Crit folder management
error_log_path = f'{root_folder}/Logs'
if not os.path.exists(error_log_path):
os.mkdir(error_log_path)

weights_file_path = f'{root_folder}/Dependencies/{weights_file_name}'
if not os.path.exists(f'{root_folder}/Dependencies'):
os.mkdir(f'{root_folder}/Dependencies')

if not os.path.exists(weights_file_path):
raise Exception(f'El archivo de pesos "{weights_file_name}" no pudo ser encontrado, ruta: '
f'{weights_file_path}')

# Set Network & Architecture
model = mAlexNet()
model.load_state_dict(torch.load(weights_file_path, map_location=torch.device('cpu')))

# Set pre-processing format
preprocess = T.Compose([
T.Resize(256),
T.CenterCrop(224),
T.ToTensor(),
T.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# Master except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
msg = f'[{exc_tb.tb_lineno}] = {error}'
try:
if not error_log_path:
error_log_path = '.'
except NameError:
error_log_path = '.'
helpers.append_log_message(msg, error_log_path)


# Main
if __name__ == '__main__':
try:
true_list = ['true', '1', 't', 'y', 'yes']

# Set end hour
if not ':' in hora_cierre:
raise Exception(
'Error, la etiqueta "hora_autocerrado" del archivo XML no sigue el formato: "00:00" (min:sec)')
hora_list = hora_cierre.split(':')
break_time = datetime.now().replace(hour=int(hora_list[0]), minute=int(hora_list[1]), second=0,
microsecond=0)

# Folder management
parking_images_folder = f'{root_folder}/Zones_images'
if not os.path.exists(parking_images_folder):
os.mkdir(parking_images_folder)

spots_images_folder = f'{root_folder}/Zones_images/Temp_spots'
if not os.path.exists(spots_images_folder):
os.mkdir(spots_images_folder)

error_log_path = f'{root_folder}/Logs'
if not os.path.exists(error_log_path):
os.mkdir(error_log_path)

if not os.path.exists(f'{root_folder}/Executables'):
os.mkdir(f'{root_folder}/Executables')

if not os.path.exists(f'{root_folder}/Documentation'):
os.mkdir(f'{root_folder}/Documentation')

if not os.path.exists(f'{root_folder}/Scripts'):
os.mkdir(f'{root_folder}/Scripts')

if almacenar_lugares in true_list:
spots_storage = f'{root_folder}/Spots_images'
if not os.path.exists(spots_storage):
os.mkdir(spots_storage)

if not os.path.exists(f'{spots_storage}/Available'):
os.mkdir(f'{spots_storage}/Available')

if not os.path.exists(f'{spots_storage}/Occupied'):
os.mkdir(f'{spots_storage}/Occupied')

# Create tables in db if needed
check_db()

# Insert zones or spots in db if needed
check_db_spots(parking_zones)

infinite = True
while infinite:

# End execution
current_time = datetime.now()
if current_time >= break_time:
break

# Get_folder_images
lista_rutas_imagenes = helpers.get_file_names(parking_images_folder, file_extension)

if lista_rutas_imagenes:

if procesamiento_paralelo not in true_list:
# # ejecucion secuencial
for i, path in enumerate(lista_rutas_imagenes):
process_zone(path)

else:
# # Ejecucion paralela
with concurrent.futures.ProcessPoolExecutor() as executor:
future = executor.map(process_zone, lista_rutas_imagenes)

clean_folder(parking_images_folder, file_extension)

time.sleep(int(tiempo_espera))

# Master except
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
msg = f'[{exc_tb.tb_lineno}] = {error}'
try:
if not error_log_path:
error_log_path = '.'
except NameError:
error_log_path = '.'
helpers.append_log_message(msg, error_log_path)
try:
helpers.send_email(emails, 'Se ha producido un error en el aplicativo de detección de zonas para parqueo',
error)
except NameError:
helpers.append_log_message('No se pudo enviar el correo electrónico ', error_log_path)