""" DF-Agg """
# df-aggregator, networked radio direction finding software.
#     Copyright (C) 2020 Corey Koval
#
#     This program is free software: you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with this program.  If not, see <https://www.gnu.org/licenses/>.
import asyncio
import logging
import sys

import vincenty as v
import numpy as np
import math
import time
import sqlite3
import threading
import signal
import json
from colorsys import hsv_to_rgb
from optparse import OptionParser
from lxml import etree
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler, minmax_scale
from geojson import MultiPoint, Feature, FeatureCollection
from czml3 import Packet, Document, Preamble
from czml3.properties import (
    Position, Polyline, PolylineMaterial, PolylineOutlineMaterial,
    PolylineDashMaterial, Color, Material
)
from multiprocessing import Process, Queue, Value, Lock, RLock
from bottle import (route, run, request, get, put, response,
                    redirect, template, static_file)

from sys import version_info

from utils.database_writer import DatabaseWriter
from utils.ether_service.utils.receivers_controller import \
    ReceiverController
from utils.ether_service.ether_service import EtherService
from utils.ether_service.utils.log import init_logging
from utils.ws_client import WSClient


logger = logging.getLogger(__name__)


if version_info.major != 3 or version_info.minor < 6:
    logger.error("Looks like you're running python version "
                 f"{version_info.major}.{version_info.minor} "
                 "which is no longer supported.")
    logger.error("Your python version is out of date, "
                 "please update to 3.6 or newer.")
    sys.exit(1)


DBSCAN_Q = Queue()
DBSCAN_WAIT_Q = Queue()
DATABASE_EDIT_Q = Queue()
DATABASE_RETURN = Queue()

d = 40000  # draw distance of LOBs in meters
heading_d = 20000
max_age = 5000
# receivers = []
shared_dict = dict(response=None)


# noinspection PyPep8Naming,SpellCheckingInspection
class math_settings:
    """ Stores settings related to intersect capture and post-processing. """
    def __init__(self, eps, min_samp, conf, power):
        self.eps = eps
        self.min_samp = min_samp
        self.min_conf = conf
        self.min_power = power

    rx_busy = True
    receiving = True
    plotintersects = False


class Context:
    """ This is an app context.
        We use this crap cause we are working with bad df-agg code """
    db_writer = None
    ws_client = None
    receiver_controller = None
    receivers_queue = Queue()
    # queues


# noinspection PyPep8Naming
class receiver:
    """ Stores all variables pertaining to a reveiver.
        Also updates receiver variable upon request."""
    latitude = 0.0
    longitude = 0.0
    heading = 0.0
    raw_doa = 0.0
    doa = 0.0
    frequency = 0.0
    power = 0.0
    confidence = 0
    doa_time = 0
    isMobile = False
    isSingle = False
    previous_doa_time = 0
    last_processed_at = 0
    d_2_last_intersection = [d]

    def __init__(self, station_id):
        self.station_id = station_id
        self.isAuto = True
        self.isActive = True
        self.flipped = False
        self.inverted = True

        # self.update(first_run=True)  # Don't do this! Never, ever!

        # init default values
        self.latitude = 0.0
        self.longitude = 0.0
        self.heading = 0.0
        self.raw_doa = 0.0
        self.doa = 0.0
        self.frequency = 0.0
        self.power = 0.0
        self.confidence = 0
        self.doa_time = 0

    # noinspection PyUnresolvedReferences
    def wr_xml(self, station_id, doa, conf, pwr, freq,
               latitude, longitude, heading):
        # Kerberos-ify the data
        confidence_str = "{}".format(np.max(int(float(conf) * 100)))
        max_power_level_str = "{:.1f}".format((np.maximum(-100, float(pwr) + 100)))

        epoch_time = int(1000 * round(time.time(), 3))
        # create the file structure
        ET = {}
        data = ET.Element('DATA')
        xml_st_id = ET.SubElement(data, 'STATION_ID')
        xml_time = ET.SubElement(data, 'TIME')
        xml_freq = ET.SubElement(data, 'FREQUENCY')
        xml_location = ET.SubElement(data, 'LOCATION')
        xml_latitide = ET.SubElement(xml_location, 'LATITUDE')
        xml_longitude = ET.SubElement(xml_location, 'LONGITUDE')
        xml_heading = ET.SubElement(xml_location, 'HEADING')
        xml_doa = ET.SubElement(data, 'DOA')
        xml_pwr = ET.SubElement(data, 'PWR')
        xml_conf = ET.SubElement(data, 'CONF')

        xml_st_id.text = str(station_id)
        xml_time.text = str(epoch_time)
        xml_freq.text = str(freq / 1000000)
        xml_latitide.text = str(latitude)
        xml_longitude.text = str(longitude)
        xml_heading.text = str(heading)
        xml_doa.text = doa
        xml_pwr.text = max_power_level_str
        xml_conf.text = confidence_str

        # create a new XML file with the results
        html_str = ET.tostring(data, encoding="unicode")
        self.DOA_res_fd.seek(0)
        self.DOA_res_fd.write(html_str)
        self.DOA_res_fd.truncate()
        # print("Wrote XML")

    # Updates receiver from the remote URL
    def update(self, first_run=False):
        """ update """
        return
        try:
            xml_contents = etree.parse(self.station_url)
            xml_station_id = xml_contents.find('STATION_ID')
            self.station_id = xml_station_id.text
            xml_doa_time = xml_contents.find('TIME')
            self.doa_time = int(xml_doa_time.text)
            xml_freq = xml_contents.find('FREQUENCY')
            self.frequency = float(xml_freq.text)
            xml_latitude = xml_contents.find('LOCATION/LATITUDE')
            self.latitude = float(xml_latitude.text)
            xml_longitude = xml_contents.find('LOCATION/LONGITUDE')
            self.longitude = float(xml_longitude.text)
            xml_heading = xml_contents.find('LOCATION/HEADING')
            self.heading = float(xml_heading.text)
            xml_doa = xml_contents.find('DOA')
            self.raw_doa = float(xml_doa.text)
            if self.inverted:
                self.doa = self.heading + (360 - self.raw_doa)
            elif self.flipped:
                self.doa = self.heading + (180 + self.raw_doa)
            else:
                self.doa = self.heading + self.raw_doa
            if self.doa < 0:
                self.doa += 360
            elif self.doa > 359:
                self.doa -= 360
            xml_power = xml_contents.find('PWR')
            self.power = float(xml_power.text)
            xml_conf = xml_contents.find('CONF')
            self.confidence = int(xml_conf.text)
        except Exception as ex:
            # TODO(s1z): This is a bullshit handler!
            #            Has to be changed to appropriate actions depend on
            #            exception we get.
            #            It doesn't make any sense to turn off a receiver if
            #            there was something wrong with the data/network/etc!
            #            So i've added check. If we have isActive == True, then
            #            we do nothing with the receiver.

            # TODO(s1z): I've left this line just to show
            #            that there's something wrong with the receiver!

            if self.isActive:
                return

            self.latitude = 0.0
            self.longitude = 0.0
            self.heading = 0.0
            self.raw_doa = 0.0
            self.doa = 0.0
            self.frequency = 0.0
            self.power = 0.0
            self.confidence = 0
            self.doa_time = 0
            self.isActive = False
            exc_info = (type(ex), ex, ex.__traceback__)
            logger.error("Error occurred", exc_info=exc_info)
            logger.error(f"Problem connecting to {self.station_id}, "
                         f"receiver deactivated. Reactivate in WebUI.")

    def receiver_dict(self):
        """ Returns receivers properties as a dict,
            useful for passing data to the WebUI"""
        return {'stationId': self.station_id,
                'latitude': self.latitude,
                'longitude': self.longitude,
                'heading': self.heading,
                'doa': self.doa,
                'frequency': self.frequency,
                'power': self.power,
                'confidence': self.confidence,
                'doa_time': self.doa_time,
                'mobile': self.isMobile,
                'active': self.isActive,
                'auto': self.isAuto,
                'inverted': self.inverted,
                'single': self.isSingle}

    def lob_length(self):
        """ lob_length """
        if self.d_2_last_intersection:
            return round(max(self.d_2_last_intersection)) + 200
        else:
            return d


def plot_polar(lat_a, lon_a, lat_a2, lon_a2):
    """ Converts Lat/Lon to polar coordinates """
    # Convert points in great circle 1, degrees to radians
    p1_lat1_rad = math.radians(lat_a)
    p1_long1_rad = math.radians(lon_a)
    p1_lat2_rad = math.radians(lat_a2)
    p1_long2_rad = math.radians(lon_a2)

    # Put in polar coordinates
    x1 = math.cos(p1_lat1_rad) * math.cos(p1_long1_rad)
    y1 = math.cos(p1_lat1_rad) * math.sin(p1_long1_rad)
    z1 = math.sin(p1_lat1_rad)
    x2 = math.cos(p1_lat2_rad) * math.cos(p1_long2_rad)
    y2 = math.cos(p1_lat2_rad) * math.sin(p1_long2_rad)
    z2 = math.sin(p1_lat2_rad)

    return ([x1, y1, z1], [x2, y2, z2])


#####################################################
# Find line of intersection between two great circles
#####################################################
def plot_intersects(lat_a, lon_a, doa_a, lat_b, lon_b, doa_b,
                    max_distance=100000):
    # plot another point on the lob
    # v.direct(lat_a, lon_a, doa_a, d)
    # returns (lat_a2, lon_a2)

    # Get normal to planes containing great circles
    # np.cross product of vector to each point from the origin
    coord_a2 = v.direct(lat_a, lon_a, doa_a, d)
    coord_b2 = v.direct(lat_b, lon_b, doa_b, d)
    plane_a = plot_polar(lat_a, lon_a, *coord_a2)
    plane_b = plot_polar(lat_b, lon_b, *coord_b2)
    N1 = np.cross(plane_a[0], plane_a[1])
    N2 = np.cross(plane_b[0], plane_b[1])

    # Find line of intersection between two planes
    L = np.cross(N1, N2)
    # Find two intersection points
    X1 = L / np.sqrt(L[0] ** 2 + L[1] ** 2 + L[2] ** 2)
    X2 = -X1

    def mag(q):
        return np.sqrt(np.vdot(q, q))

    dist1 = mag(X1 - plane_a[0])
    dist2 = mag(X2 - plane_a[0])
    # return the (lon_lat pair of the closer intersection)
    if dist1 < dist2:
        i_lat = math.asin(X1[2]) * 180. / np.pi
        i_long = math.atan2(X1[1], X1[0]) * 180. / np.pi
    else:
        i_lat = math.asin(X2[2]) * 180. / np.pi
        i_long = math.atan2(X2[1], X2[0]) * 180. / np.pi

    check_bearing = v.get_heading((lat_a, lon_a), (i_lat, i_long))

    if abs(check_bearing - doa_a) < 5:
        km = v.inverse([lat_a, lon_a], [i_lat, i_long])
        if km[0] < max_distance:
            return i_lat, i_long
        else:
            return None


def do_dbscan(x, epsilon, min_samp):
    """ We start this in it's own process do it doesn't eat all of your RAM.
        This becomes noticable at over 10k intersections."""
    DBSCAN_WAIT_Q.put(True)
    db = DBSCAN(eps=epsilon, min_samples=min_samp).fit(x)
    DBSCAN_Q.put(db.labels_)
    if not DBSCAN_WAIT_Q.empty():
        DBSCAN_WAIT_Q.get()


def autoeps_calc(xs):
    """ Auto calculate the best eps value. """
    # only use a sample of the data to speed up calculation.
    xs = xs[:min(2000, len(xs)):2]
    min_distances = []
    for x in xs:
        distances = []
        for y in xs:
            # calculate euclidian distance
            distance = math.sqrt(sum([(a - b) ** 2 for a, b in zip(x, y)]))
            if distance > 0:
                distances.append(distance)
        min_distances.extend(np.sort(distances)[0:3].tolist())

    sorted_distances = np.sort(min_distances).tolist()
    try:
        for x1, y1 in enumerate(sorted_distances):
            x2 = x1 + 1
            y2 = sorted_distances[x2]
            # calculate slope
            m = (y2 - y1) / (x2 - x1)

            # once the slope starts getting steeper, use that as the eps value
            if m > 0.003:
                # print(f"Slope: {round(m, 3)}, eps: {y1}")
                return y1
    except IndexError:
        return 0


# noinspection PyShadowingNames
def process_data(database_name, epsilon, min_samp):
    """ Computes DBSCAN Alorithm is applicable,
        finds the mean of a cluster of intersections."""
    n_std = 3.0
    intersect_list = []
    likely_location = []
    ellipsedata = []
    # weighted_location = []
    conn = sqlite3.connect(database_name)
    curs = conn.cursor()
    curs.execute("SELECT DISTINCT aoi_id FROM intersects")
    curs.execute('SELECT uid FROM interest_areas WHERE aoi_type="aoi"')
    aoi_list = [item for sublist in curs.fetchall() for item in sublist]
    # aoi_list = [-1] if len(aoi_list) == 0 else aoi_list
    aoi_list.append(-1)

    for aoi in aoi_list:
        logging.debug(f"Checking AOI {aoi}.")
        curs.execute('''SELECT longitude, latitude, time FROM intersects
            WHERE aoi_id=? ORDER BY confidence DESC LIMIT 25000''', [aoi])
        intersect_array = np.array(curs.fetchall())
        if intersect_array.size != 0:
            if epsilon != "0":
                X = StandardScaler().fit_transform(intersect_array[:, 0:2])
                n_points = len(X)

                if min_samp == "auto":
                    min_samp = round(0.05 * n_points)
                elif not min_samp.isnumeric():
                    break

                min_samp = max(3, min_samp)
                min_samp = int(min_samp)

                if epsilon == "auto":
                    epsilon = autoeps_calc(X)
                    logger.debug(f"min_samp: {min_samp}, eps: {epsilon}")
                else:
                    try:
                        epsilon = float(epsilon)
                    except ValueError:
                        break

                # size_x = sys.getsizeof(X)/1024
                # print(f"The dataset is {size_x} kilobytes")
                logger.info(f"Computing Clusters "
                            f"from {n_points} intersections.")
                while not DBSCAN_WAIT_Q.empty():
                    logger.debug("Waiting for my turn...")
                    time.sleep(1)
                starttime = time.time()
                db = Process(target=do_dbscan, args=(X, epsilon, min_samp))
                db.daemon = True
                db.start()
                # noinspection PyBroadException
                try:
                    labels = DBSCAN_Q.get(timeout=10)
                    db.join()
                except Exception:
                    logger.warning("DBSCAN took took long, terminated.")
                    if not DBSCAN_WAIT_Q.empty():
                        DBSCAN_WAIT_Q.get()
                    db.terminate()
                    return likely_location, intersect_list, ellipsedata

                stoptime = time.time()
                logger.info(f"DBSCAN took {stoptime - starttime}"
                            f"seconds to compute the clusters.")

                intersect_array = np.column_stack((intersect_array, labels))

                # Number of clusters in labels, ignoring noise if present.
                n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)
                n_noise_ = list(labels).count(-1)
                logger.debug('Number of clusters: %d' % n_clusters_)
                logger.debug('Outliers Removed: %d' % n_noise_)

                for x in range(n_clusters_):
                    cluster = np.array([]).reshape(0, 3)
                    for y in range(len(intersect_array)):
                        if intersect_array[y][-1] == x:
                            cluster = np.concatenate(
                                (cluster, [intersect_array[y][0:-1]]), axis=0)
                    # weighted_location.append(np.average(cluster[:,0:2],
                    #                          weights=cluster[:,2],
                    #                          axis=0).tolist())
                    clustermean = np.mean(cluster[:, 0:2], axis=0)
                    likely_location.append(clustermean.tolist())
                    cov = np.cov(cluster[:, 0], cluster[:, 1])
                    a = cov[0, 0]
                    b = cov[0, 1]
                    c = cov[1, 1]
                    if a == 0.0 or b == 0.0 or c == 0.0:
                        if debugging:
                            logger.debug(f"A: {a} B: {b} C: {c}")
                            logger.debug("Unable to resolve ellipse.")
                        break
                    lam1 = a + c / 2 + np.sqrt((a - c / 2) ** 2 + b ** 2)
                    # lam2 = a+c/2 - np.sqrt((a-c/2)**2 + b**2)
                    # print([lam1, lam2, a, c])
                    pearson = b / np.sqrt(a * c)
                    if 1 + pearson < 0.0 or 1 - pearson < 0.0:
                        if debugging:
                            logger.debug(f"Pearson Value: {pearson}")
                            logger.debug("Unable to resolve ellipse.")
                        break
                    logger.debug(f"A: {a} B: {b} C: {c} pearson: {pearson}")
                    ell_radius_x = np.sqrt(1 + pearson) * np.sqrt(a) * n_std
                    ell_radius_y = np.sqrt(1 - pearson) * np.sqrt(c) * n_std
                    axis_x = v.inverse(clustermean.tolist()[
                                       ::-1], (ell_radius_x + clustermean[1],
                                               clustermean[0]))[0]
                    axis_y = v.inverse(clustermean.tolist()[
                                       ::-1], (clustermean[1],
                                               ell_radius_y + clustermean[0]))[
                        0]
                    if b == 0 and a >= c:
                        rotation = 0
                    elif b == 0 and a < c:
                        rotation = np.pi / 2
                    else:
                        rotation = math.atan2(lam1 - a, b)

                    ellipsedata.append(
                        [axis_x, axis_y, rotation, *clustermean.tolist()])

                for x in likely_location:
                    logger.debug(x[::-1])

            for x in intersect_array:
                try:
                    if x[-1] >= 0:
                        intersect_list.append(x[0:3].tolist())
                except IndexError:
                    intersect_list.append(x.tolist())

        else:
            logger.debug(f"No Intersections in AOI {aoi}.")
    conn.close()
    return likely_location, intersect_list, ellipsedata


def purge_database(type, lat, lon, radius):
    """ Checks interesections stored in the database against a lat/lon/radius
        and removes items inside exclusion areas."""
    conn = sqlite3.connect(database_name)
    c = conn.cursor()
    c.execute("SELECT latitude, longitude, id FROM intersects")
    intersect_list = c.fetchall()
    conn.close()
    delete_these = []
    purge_count = 0
    for x in intersect_list:
        if type == "exclusion":
            distance = v.inverse(x[0:2], (lat, lon))[0]
            if distance < radius:
                delete_these.append((x[2],))
                purge_count += 1

    command = "DELETE FROM intersects WHERE id=?"
    DATABASE_EDIT_Q.put((command, delete_these, False))
    # DATABASE_RETURN.get(timeout=1)
    DATABASE_EDIT_Q.put(("done", None, False))
    logger.info(f"I purged {purge_count} intersects.")


###############################################
# Checks interesections stored in the database
# against a lat/lon/radius and removes items
# that don't match the rules.
###############################################
@get("/run_all_aoi_rules")
def run_aoi_rules():
    purged = 0
    sorted = 0
    in_aoi = None
    aoi_list = fetch_aoi_data()
    conn = sqlite3.connect(database_name)
    c = conn.cursor()
    c.execute('SELECT id, latitude, longitude FROM intersects')
    intersect_list = c.fetchall()
    c.execute('SELECT COUNT(*) FROM interest_areas WHERE aoi_type="aoi"')
    n_aoi = c.fetchone()[0]
    conn.close()
    starttime = time.time()
    del_list = []
    keep_list = []
    if n_aoi == 0:
        command = "UPDATE intersects SET aoi_id=?"
        DATABASE_EDIT_Q.put((command, (-1,), True))
        DATABASE_EDIT_Q.put(("done", None, False))
        DATABASE_RETURN.get(timeout=1)
    else:
        for point in intersect_list:
            keep_me = []
            id, lat, lon = point
            for x in aoi_list:
                # aoi = {
                # 'uid': x[0],
                # 'aoi_type': x[1],
                # 'latitude': x[2],
                # 'longitude': x[3],
                # 'radius': x[4]
                # }
                distance = v.haversine(x[2], x[3], lat, lon)
                if x[1] == "exclusion":
                    if distance < x[4]:
                        keep_me = [False]
                        break
                elif x[1] == "aoi":
                    if distance < x[4]:
                        sorted += 1
                        keep_me.append(True)
                        in_aoi = x[0]
                    else:
                        keep_me.append(False)
                        # del_list.append(id)

            if not any(keep_me):
                del_list.append((id,))
                purged += 1
            else:
                keep_list.append((in_aoi, id))

    command = "DELETE from intersects WHERE id=?"
    DATABASE_EDIT_Q.put((command, del_list, True))
    DATABASE_RETURN.get()
    DATABASE_EDIT_Q.put(("done", None, False))

    command = "UPDATE intersects SET aoi_id=? WHERE id=?"
    DATABASE_EDIT_Q.put((command, keep_list, True))
    DATABASE_RETURN.get()
    DATABASE_EDIT_Q.put(("done", None, False))

    stoptime = time.time()
    logger.info(f"Purged {purged} intersections and sorted {sorted}"
                f"intersections into {n_aoi} AOIs"
                f"in {stoptime - starttime} seconds.")
    return "OK"


###############################################
# Writes a geojson file upon request.
###############################################
def write_geojson(best_point, all_the_points):
    all_pt_style = {"name": "Various Intersections", "marker-color": "#FF0000"}
    best_pt_style = {"name": "Most Likely TX Location",
                     "marker-color": "#00FF00"}
    if all_the_points is not None:
        all_the_points = Feature(
            properties=all_pt_style,
            geometry=MultiPoint(tuple(all_the_points)))
        with open(geofile, "w") as file1:
            if best_point is not None:
                reversed_best_point = []
                for x in best_point:
                    reversed_best_point.append(x)
                best_point = Feature(properties=best_pt_style,
                                     geometry=MultiPoint(
                                         tuple(reversed_best_point)))
                file1.write(str(FeatureCollection(
                    [best_point, all_the_points])))
            else:
                file1.write(str(FeatureCollection([all_the_points])))
        logger.debug(f"Wrote file {geofile}")


# noinspection PyPep8Naming
def write_czml(best_point, all_the_points, ellipsedata,
               plot_all_intersects, eps):
    """ Writes output.czml used by the WebUI """
    point_properties = {
        "pixelSize": 5.0,
        "heightReference": "CLAMP_TO_GROUND",
        "zIndex": 3
    }
    best_point_properties = {
        "pixelSize": 12.0,
        "zIndex": 10,
        "heightReference": "CLAMP_TO_GROUND",
        "color": {
            "rgba": [0, 255, 0, 255],
        }
    }

    ellipse_properties = {
        "granularity": 0.008722222,
        "zIndex": 5,
        "material": {
            "solidColor": {
                "color": {
                    "rgba": [255, 0, 0, 90]
                }
            }
        }
    }

    top = Preamble(name="Geolocation Data")
    all_point_packets = []
    best_point_packets = []
    ellipse_packets = []

    if len(all_the_points) > 0 and (plot_all_intersects or eps == "0"):
        all_the_points = np.array(all_the_points)
        scaled_time = minmax_scale(all_the_points[:, -1])
        all_the_points = np.column_stack((all_the_points, scaled_time))
        for x in all_the_points:
            # rgb = hsvtorgb(x[-1]/3, 0.9, 0.9)
            rgb = map(lambda c: int(c * 255), hsv_to_rgb(x[-1] / 3, 0.9, 0.9))
            color_property = {"color": {"rgba": [*rgb, 255]}}
            all_point_packets.append(Packet(id=str(x[1]) + ", " + str(x[0]),
                                            point={**point_properties,
                                                   **color_property},
                                            position={
                                                "cartographicDegrees": [x[0],
                                                                        x[1],
                                                                        0]},
                                            ))

    if len(best_point) > 0:
        for x in best_point:
            gmaps_url = (f"https://www.google.com/maps/dir/?api=1&"
                         f"destination={x[1]},+{x[0]}&travelmode=driving")
            best_point_packets.append(
                Packet(id=str(x[1]) + ", " + str(x[0]),
                       point=best_point_properties,
                       description=(f"<a href='{gmaps_url}' target='_blank'>"
                                    f"Google Maps Directions</a>"),
                       position={"cartographicDegrees": [x[0], x[1], 0]})
            )

    if len(ellipsedata) > 0:
        for x in ellipsedata:
            # rotation = 2 * np.pi - x[2]
            if x[0] >= x[1]:
                # rotation = x[2]
                semiMajorAxis = x[0]
                semiMinorAxis = x[1]
                rotation = 2 * np.pi - x[2]
                rotation += np.pi / 2
                # print(f"{x[2]} Inverted to: {rotation}")
                # print(f"SemiMajor: {semiMajorAxis},
                #       f"Semiminor: {semiMinorAxis}")
                # print(f"{x[4], x[3]} is inveted")
            else:
                rotation = x[2]
                semiMajorAxis = x[1]
                semiMinorAxis = x[0]
                # print(f"Not inverted: {rotation}")
                # print(f"SemiMajor: {semiMajorAxis},"
                #       f"Semiminor: {semiMinorAxis}")
                # print(f"{x[4], x[3]} is NOT inveted")

            ellipse_info = {"semiMajorAxis": semiMajorAxis,
                            "semiMinorAxis": semiMinorAxis,
                            "rotation": rotation}
            ellipse_packets.append(Packet(id=str(x[4]) + ", " + str(x[3]),
                                          ellipse={
                                              **ellipse_properties,
                                              **ellipse_info},
                                          position={
                                              "cartographicDegrees": [x[3],
                                                                      x[4],
                                                                      0]}))
    document_list = [top, ]
    document_list.extend(best_point_packets)
    document_list.extend(all_point_packets)
    document_list.extend(ellipse_packets)
    return Document(document_list).dumps(separators=(',', ':'))


@get('/receivers.czml')
def write_rx_czml():
    """ Writes receivers.czml used by the WebUI """
    response.set_header(
        'Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0',
    )
    response.set_header(
        'Content-Type', 'application/json'
    )
    height = 50
    min_conf = ms.min_conf
    min_power = ms.min_power
    green = [0, 255, 0, 255]
    orange = [255, 140, 0, 255]
    red = [255, 0, 0, 255]
    gray = [128, 128, 128, 255]
    receiver_point_packets = []
    lob_packets = []
    top = Preamble(name="Receivers")

    rx_properties = {
        "verticalOrigin": "BOTTOM",
        "zIndex": 9,
        "scale": 0.75,
        "heightReference": "CLAMP_TO_GROUND",
        "height": 48,
        "width": 48,
    }
    receivers_doa = Context.receiver_controller.get_doa()
    for main_index, (station_id, doa_list) in enumerate(receivers_doa.items()):
        for slave_index, doa in enumerate(doa_list):
            index = main_index * 10 + slave_index
            confidence = np.max(int(float(doa.confidence) * 100))
            power = np.maximum(-100, float(doa.power) + 100)

            # logging.info(f" {station_id}: {doa.degrees}º ")
            if confidence > min_conf and power > min_power:
                lob_color = green
            elif confidence <= min_conf and power > min_power:
                lob_color = orange
            else:
                lob_color = red

            lob_start_lat = doa.latitude
            lob_start_lon = doa.longitude
            lob_stop_lat, lob_stop_lon = v.direct(
                lob_start_lat, lob_start_lon, doa.degrees, 60200
            )
            lob_packets.append(
                Packet(
                    id=f"LOB-{station_id}-{index}",
                    polyline=Polyline(
                        material=Material(
                            polylineOutline=PolylineOutlineMaterial(
                                color=Color(rgba=lob_color),
                                outlineColor=Color(rgba=[0, 0, 0, 255]),
                                outlineWidth=2
                            )
                        ),
                        clampToGround=True,
                        width=5,
                        positions=Position(
                            cartographicDegrees=[
                                lob_start_lon,
                                lob_start_lat,
                                height,
                                lob_stop_lon,
                                lob_stop_lat,
                                height
                            ]
                        )
                    )
                )
            )
            heading_start_lat = doa.latitude
            heading_start_lon = doa.longitude
            heading_stop_lat, heading_stop_lon = v.direct(
                heading_start_lat, heading_start_lon, doa.heading, heading_d
            )
            lob_packets.append(
                Packet(
                    id=f"HEADING-{station_id}-{index}",
                    polyline=Polyline(
                        material=PolylineMaterial(
                            polylineDash=PolylineDashMaterial(
                                color=Color(rgba=gray),
                                gapColor=Color(rgba=[0, 0, 0, 0])
                            )
                        ),
                        clampToGround=True,
                        width=2,
                        positions=Position(
                            cartographicDegrees=[
                                heading_start_lon,
                                heading_start_lat,
                                height,
                                heading_stop_lon,
                                heading_stop_lat,
                                height]
                        )
                    )
                )
            )

            rx_icon = {"image": {"uri": "/static/tower.svg"}}
            receiver_point_packets.append(
                Packet(id=f"{station_id}-{index}",
                       billboard={**rx_properties, **rx_icon},
                       position={"cartographicDegrees": [doa.longitude,
                                                         doa.latitude, 15]})
            )

    document_list = [top, ]
    document_list.extend(receiver_point_packets)
    document_list.extend(lob_packets)
    response_data = Document(document_list).dumps(separators=(',', ':'))
    shared_dict["response"] = response_data
    return response_data


@get("/aoi.czml")
def wr_aoi_czml():
    """ Writes aoi.czml used by the WebUI """
    response.set_header(
        'Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
    aoi_packets = []
    top = Preamble(name="AOIs")
    area_of_interest_properties = {
                                      "granularity": 0.008722222,
                                      "height": 0,
                                      # "zIndex": 1,
                                      "material": {
                                          "solidColor": {
                                              "color": {
                                                  "rgba": [0, 0, 255, 25]
                                              }
                                          }
                                      },
                                      "outline": True,
                                      "outlineWidth": 2,
                                      "outlineColor": {
                                          "rgba": [53, 184, 240, 255], },
                                  },

    exclusion_area_properties = {
                                    "granularity": 0.008722222,
                                    "height": 0,
                                    # "zIndex": 0,
                                    "material": {
                                        "solidColor": {
                                            "color": {
                                                "rgba": [242, 10, 0, 25]
                                            }
                                        }
                                    },
                                    "outline": True,
                                    "outlineWidth": 2,
                                    "outlineColor": {
                                        "rgba": [224, 142, 0, 255], },
                                },

    for x in fetch_aoi_data():
        aoi = {
            'uid': x[0],
            'aoi_type': x[1],
            'latitude': x[2],
            'longitude': x[3],
            'radius': x[4]
        }
        if aoi['aoi_type'] == "aoi":
            aoi_properties = area_of_interest_properties[0]
        elif aoi['aoi_type'] == "exclusion":
            aoi_properties = exclusion_area_properties[0]
        aoi_info = {"semiMajorAxis": aoi['radius'],
                    "semiMinorAxis": aoi['radius'], "rotation": 0}
        aoi_packets.append(Packet(id=aoi['aoi_type'] + str(aoi['uid']),
                                  ellipse={**aoi_properties, **aoi_info},
                                  position={
                                      "cartographicDegrees": [aoi['longitude'],
                                                              aoi['latitude'],
                                                              0]}))

    return Document([top] + aoi_packets).dumps(separators=(',', ':'))


@route('/static/<filepath:path>', name='static')
def server_static(filepath):
    """ Serves static files such as CSS and JS to the WebUI """
    response = static_file(filepath, root='./static')
    response.set_header(
        'Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
    return response


@get('/')
@get('/index')
@get('/cesium')
def cesium():
    """ Loads the main page of the WebUI http://[ip]:[port]/ """
    response.set_header(
        'Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
    return template(
        'cesium.tpl',
        {
            'access_token': access_token,
            'epsilon': ms.eps,
            'minpower': ms.min_power,
            'minconf': ms.min_conf,
            'minpoints': ms.min_samp,
            'rx_state': "checked" if ms.receiving is True else "",
            'intersect_state': "checked" if ms.plotintersects is True else "",
            'receivers': Context.receiver_controller.receivers
        }
    )


# noinspection SpellCheckingInspection
@get('/update')
def update_cesium():
    """ GET Request to update parameters from the
        UI sliders. Not meant to be user facing. """
    # eps = float(request.query.eps) if request.query.eps else ms.eps
    # min_samp = (float(request.query.minpts)
    #             if request.query.minpts
    #             else ms.min_samp)
    ms.min_conf = float(
        request.query.minconf) if request.query.minconf else ms.min_conf
    ms.min_power = float(
        request.query.minpower) if request.query.minpower else ms.min_power

    if request.query.rx == "true":
        ms.receiving = True
    elif request.query.rx == "false":
        ms.receiving = False

    # if request.query.plotpts == "true":
    #     ms.plotintersects = True
    # elif request.query.plotpts == "false":
    #     ms.plotintersects = False

    return "OK"


# noinspection PyShadowingNames
@get('/rx_params')
def rx_params():
    """ Returns a JSON file to the WebUI
        with information to fill in the RX cards. """
    all_rx = {'receivers': {}}
    rx_properties = []
    for index, x in enumerate(Context.receiver_controller.receivers):
        # x.update()  # this line blocks web execution, should be async!
        rx = x.receiver_dict()
        rx['uid'] = index
        rx_properties.append(rx)
    all_rx['receivers'] = rx_properties
    response.headers['Content-Type'] = 'application/json'
    return json.dumps(all_rx)


# noinspection SpellCheckingInspection
@get('/output.czml')
def tx_czml_out():
    """ Returns a CZML file that contains intersect
        and ellipse information for Cesium. """
    eps = request.query.eps if request.query.eps else str(ms.eps)
    min_samp = request.query.minpts if request.query.minpts else str(
        ms.min_samp)
    if request.query.plotpts == "true":
        plot_all_intersects = True
    elif request.query.plotpts == "false":
        plot_all_intersects = False
    else:
        plot_all_intersects = ms.plotintersects
    response.set_header(
        'Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
    output = write_czml(*process_data(database_name, eps, min_samp),
                        plot_all_intersects, eps)
    return str(output)


# noinspection PyPep8Naming,SqlDialectInspection,PyShadowingNames
@put('/rx_params/<action>')
def update_rx(action):
    """  PUT request to update receiver variables from the WebUI """
    data = json.load(request.body)
    if action == "new":
        station_id = data.get('stationId', None)
        if station_id is not None:
            Context.receiver_controller.register_receiver(station_id)
            add_rx_db(station_id)
    elif action == "del":
        station_id = data.get("stationId", None)
        if station_id is not None:
            Context.receiver_controller.unregister_receiver(station_id)
            delete_rx_db(station_id)
    elif action == "activate":
        Context.receiver_controller.update_receiver_active(data)
    else:
        Context.receiver_controller.update_receiver(data)
    return redirect('/rx_params')


# noinspection SpellCheckingInspection
@get('/interest_areas')
def load_interest_areas():
    """ Returns a JSON file to the WebUI with
        information to fill in the AOI cards. """
    all_aoi = {'aois': {}}
    aoi_properties = []
    for x in fetch_aoi_data():
        aoi = {
            'uid': x[0],
            'aoi_type': x[1],
            'latitude': x[2],
            'longitude': x[3],
            'radius': x[4]
        }
        aoi_properties.append(aoi)
    all_aoi['aois'] = aoi_properties
    response.headers['Content-Type'] = 'application/json'
    return json.dumps(all_aoi)


# noinspection SqlNoDataSourceInspection,SqlDialectInspection
@put('/interest_areas/<action>')
def handle_interest_areas(action):
    """ PUT request to add new AOI to DB """
    data = json.load(request.body)
    if action == "new" and "" not in data.values():
        aoi_type = data['aoi_type']
        lat = data['latitude']
        lon = data['longitude']
        radius = data['radius']
        add_aoi(aoi_type, lat, lon, radius)
    elif action == "del":
        command = "UPDATE intersects SET aoi_id=? WHERE aoi_id=?"
        DATABASE_EDIT_Q.put((command, [(-1, data['uid']), ], True))
        DATABASE_RETURN.get(timeout=1)
        to_table = (str(data['uid']),)
        command = "DELETE FROM interest_areas WHERE uid=?"
        DATABASE_EDIT_Q.put((command, [to_table, ], True))
        DATABASE_RETURN.get(timeout=1)
        DATABASE_EDIT_Q.put(("done", None, False))
    elif action == "purge":
        conn = sqlite3.connect(database_name)
        c = conn.cursor()
        c.execute("SELECT aoi_type, latitude, longitude, radius"
                  "FROM interest_areas WHERE uid=?", [data['uid']])
        properties = c.fetchone()
        conn.close()
        purge_database(*properties)


class BottleWebServer(threading.Thread):
    """ Database Writer thread """

    def __init__(self, interface="127.0.0.1", port=8080):
        super().__init__(name=self.__class__.__name__, daemon=True)
        self.interface = interface
        self.port = port
        self._is_running = False
        self._is_running_lock = threading.Lock()

    @property
    def is_running(self):
        """ Get _is_running safely """
        with self._is_running_lock:
            return self._is_running

    @is_running.setter
    def is_running(self, value: bool):
        """ set _is_running safely """
        with self._is_running_lock:
            self._is_running = value

    def stop(self):
        """ Stop the Thread """
        self.is_running = False

    def start(self):
        """ Start thread """
        self.is_running = True
        super().start()

    def run(self):
        """ Starts the Bottle web server. """
        # noinspection PyUnresolvedReferences
        from bottle.ext.websocket import GeventWebSocketServer

        try:
            run(host=self.interface, port=self.port, quiet=True,
                server=GeventWebSocketServer, debug=True)
        except OSError:
            logger.error(f"Port {self.port} seems to be in use."
                         f"Please select another port or "
                         f"check if another instance of DFA is "
                         f"already running.")
            sys.exit(1)


# noinspection PyShadowingNames,SqlDialectInspection
def run_receiver(receivers):
    """ Captures DOA data and computes intersections
        if the receiver is enabled.
        Writes the intersections to the database."""
    dots = 0

    conn = sqlite3.connect(database_name)
    c = conn.cursor()

    while ms.receiving:
        if not debugging:
            logger.info("Receiving" + dots * '.')

        # Main loop to compute intersections between multiple receivers
        intersect_list = np.array([]).reshape(0, 3)
        ms.rx_busy = True

        for rx in receivers:
            try:
                if rx.isActive:
                    # This is very bad part of the code
                    # We have to change this to the queue for each receiver.
                    # rx.update()
                    pass
            except IOError:
                logger.error("Problem connecting to receiver.")
            rx.d_2_last_intersection = []
            time.sleep(0.1)

        receivers_len = len(receivers)
        for x in range(receivers_len):
            y_receiver_id = x + 1
            for y in range(y_receiver_id, receivers_len):
                if (receivers[x].confidence >= ms.min_conf and
                        receivers[y].confidence >= ms.min_conf and
                        receivers[x].power >= ms.min_power and
                        receivers[y].power >= ms.min_power and
                        abs(receivers[x].doa_time - receivers[
                            y].doa_time) <= max_age and
                        receivers[x].frequency == receivers[y].frequency):
                    intersection = plot_intersects(receivers[x].latitude,
                                                   receivers[x].longitude,
                                                   receivers[x].doa,
                                                   receivers[y].latitude,
                                                   receivers[y].longitude,
                                                   receivers[y].doa)
                    if intersection:
                        logger.info(f"intersection: {intersection}")
                        receivers[x].d_2_last_intersection.append(v.haversine(
                            receivers[x].latitude, receivers[x].longitude,
                            *intersection))
                        receivers[y].d_2_last_intersection.append(v.haversine(
                            receivers[y].latitude, receivers[y].longitude,
                            *intersection))
                        intersection = list(intersection)
                        avg_conf = np.mean(
                            [receivers[x].confidence, receivers[y].confidence])
                        intersection.append(avg_conf)
                        intersection = np.array([intersection])
                        if intersection.any() is not None:
                            intersect_list = np.concatenate(
                                (intersect_list, intersection), axis=0)
                # 11.01.2023::s1z: I don't know if this shit-code is blocking
                #                  or not.
                #                  But if it is, you better fucking sleep
                #                  10ms after each cycle to prevent 100% load
                #                  of the CPU.
                time.sleep(0.001)
            # 11.01.2023::s1z: I don't know if this shit-code is blocking
            #                  or not. But if it is, you better fucking sleep
            #                  10ms after each cycle to prevent 100% load
            #                  of the CPU.
            time.sleep(0.001)

        if intersect_list.size != 0:
            avg_coord = np.average(
                intersect_list[:, 0:3], weights=intersect_list[:, 2], axis=0)
            keep, in_aoi = check_aoi(*avg_coord[0:2])
            if keep:
                to_table = [receivers[x].doa_time, round(avg_coord[0], 6),
                            round(avg_coord[1], 6),
                            len(intersect_list), avg_coord[2], in_aoi]
                command = """INSERT INTO intersects
                (time, latitude, longitude, num_parents, confidence, aoi_id)
                VALUES (?,?,?,?,?,?)"""
                DATABASE_EDIT_Q.put((command, (to_table,), True))
                DATABASE_RETURN.get(timeout=1)

        # Loop to compute intersections for a single receiver
        # and update all receivers
        for rx in receivers:
            if (rx.isSingle and rx.isMobile and rx.isActive and
                    rx.confidence >= ms.min_conf and
                    rx.power >= ms.min_power and
                    rx.doa_time >= rx.previous_doa_time + 10000):
                current_doa = [rx.doa_time, rx.station_id, rx.latitude,
                               rx.longitude, rx.confidence, rx.doa]
                min_time = rx.doa_time - 1200000  # 15 Minutes
                c.execute("""SELECT latitude, longitude, confidence, lob
                             FROM lobs
                             WHERE station_id = ? AND time > ?""",
                          [rx.station_id, min_time])
                lob_array = c.fetchall()
                current_time = current_doa[0]
                lat_rxa = current_doa[2]
                lon_rxa = current_doa[3]
                conf_rxa = current_doa[4]
                doa_rxa = current_doa[5]
                keep_count = 0
                if len(lob_array) > 1:
                    for previous in lob_array:
                        lat_rxb = previous[0]
                        lon_rxb = previous[1]
                        conf_rxb = previous[2]
                        doa_rxb = previous[3]
                        spacial_diversity, z = v.inverse(
                            (lat_rxa, lon_rxa), (lat_rxb, lon_rxb))
                        min_diversity = 500
                        if (spacial_diversity > min_diversity and
                                abs(doa_rxa - doa_rxb) > 5):
                            intersection = plot_intersects(lat_rxa, lon_rxa,
                                                           doa_rxa, lat_rxb,
                                                           lon_rxb, doa_rxb)
                            if intersection:
                                intersection = list(intersection)
                                avg_conf = np.mean([conf_rxa, conf_rxb])
                                intersection.append(avg_conf)
                                keep, in_aoi = check_aoi(*intersection[0:2])
                                if keep:
                                    keep_count += 1
                                    to_table = [current_time,
                                                round(intersection[0], 5),
                                                round(intersection[1], 5),
                                                1, intersection[2], in_aoi]
                                    command = """INSERT INTO intersects
                                                 (time, latitude, longitude,
                                                 num_parents, confidence,
                                                 aoi_id)
                                                 VALUES (?,?,?,?,?,?)"""
                                    DATABASE_EDIT_Q.put((command, (to_table,),
                                                         True))
                                    DATABASE_RETURN.get(timeout=1)
                logger.info(f"Computed and kept {keep_count} intersections.")

                command = "INSERT INTO lobs VALUES (?,?,?,?,?,?)"
                DATABASE_EDIT_Q.put((command, [current_doa, ], True))
                DATABASE_RETURN.get(timeout=1)
            # 11.01.2023::s1z: I don't know if this shit-code is blocking
            #                  or not. But if it is, you better fucking sleep
            #                  10ms after each cycle to prevent 100% load
            #                  of the CPU.
            time.sleep(0.001)  # 11.01.2023::s1z: The fuck is this crap ?

            DATABASE_EDIT_Q.put(("done", None, False))
            # try:
            #     if rx.isActive: rx.update()
            # except IOError:
            #     print("Problem connecting to receiver.")

        ms.rx_busy = False
        time.sleep(0.001)
        if dots > 5:
            dots = 1
        else:
            dots += 1

    conn.close()


# noinspection SqlDialectInspection
def check_aoi(lat, lon):
    """ Checks if intersection should be kept or not """
    keep_list = []
    in_aoi = None
    conn = sqlite3.connect(database_name)
    c = conn.cursor()
    c.execute('SELECT COUNT(*) FROM interest_areas WHERE aoi_type="aoi"')
    n_aoi = c.fetchone()[0]
    conn.close()
    if n_aoi == 0:
        keep_list.append(True)
        in_aoi = -1
    for x in fetch_aoi_data():
        aoi = {
            'uid': x[0],
            'aoi_type': x[1],
            'latitude': x[2],
            'longitude': x[3],
            'radius': x[4]
        }
        distance = v.haversine(aoi['latitude'], aoi['longitude'], lat, lon)
        if aoi['aoi_type'] == "exclusion":
            if distance < aoi['radius']:
                keep = False
                return keep, in_aoi
        elif aoi['aoi_type'] == "aoi":
            if distance < aoi['radius']:
                keep_list.append(True)
                in_aoi = aoi['uid']
            else:
                keep_list.append(False)

    keep = any(keep_list)
    return keep, in_aoi


def fetch_first_or_none(query):
    """ Safe fetch one """
    result = query.fetchone()
    if result is not None and len(result) > 0:
        return result[0]
    return None


# noinspection SqlDialectInspection
def add_receiver(station_id):
    """ Adds a new receiver to the program, saves it in the database. """
    conn = sqlite3.connect(database_name)
    c = conn.cursor()
    try:
        if any(x.station_id == station_id for x in receivers):
            logger.warning("Duplicate receiver, ignoring.")
        else:
            new_receiver = receiver(station_id)
            receivers.append(new_receiver)
            new_rx = new_receiver.receiver_dict()
            to_table = [new_rx['stationId'],
                        new_rx['auto'],
                        new_rx['mobile'],
                        new_rx['single'],
                        new_rx['latitude'],
                        new_rx['longitude']]

            command = "INSERT OR IGNORE INTO receivers VALUES (?,?,?,?,?,?)"
            DATABASE_EDIT_Q.put((command, [to_table, ], True))
            DATABASE_RETURN.get(timeout=1)
            DATABASE_EDIT_Q.put(("done", None, False))
            mobile = fetch_first_or_none(
                c.execute("SELECT isMobile FROM receivers "
                          "WHERE station_id = ?", [new_rx['stationId']])
            )
            single = fetch_first_or_none(
                c.execute("SELECT isSingle FROM receivers "
                          "WHERE station_id = ?", [new_rx['stationId']])
            )
            if mobile is not None:
                new_receiver.isMobile = bool(mobile)
            if single is not None:
                new_receiver.isSingle = bool(single)
            logger.info("Created new DF Station at " + station_id)
    except AttributeError:
        pass

    conn.close()


# noinspection PyShadowingNames
def read_rx_table():
    """ Reads receivers from the database into the program. """
    conn = sqlite3.connect(database_name)
    c = conn.cursor()
    try:
        c.execute("SELECT station_url, station_id FROM receivers")
        rx_list = c.fetchall()
        for data in rx_list:
            station_url, station_id = [param.strip() for param in data]
            add_receiver(station_url, station_id)
    except sqlite3.OperationalError:
        rx_list = []
    conn.close()


# noinspection SqlDialectInspection
def add_rx_db(station_id):
    """ Create receiver in DB """
    for receiver in Context.receiver_controller.receivers:
        if receiver.station_id == station_id:
            new_rx = receiver.receiver_dict()
            to_table = [new_rx['stationId'],
                        new_rx['auto'],
                        new_rx['mobile'],
                        new_rx['single'],
                        new_rx['latitude'],
                        new_rx['longitude']]
            command = "INSERT OR IGNORE INTO receivers VALUES (?,?,?,?,?,?)"
            DATABASE_EDIT_Q.put((command, [to_table, ], True))
            DATABASE_RETURN.get(timeout=1)
            DATABASE_EDIT_Q.put(("done", None, False))
            return


# noinspection SqlDialectInspection
def delete_rx_db(station_id):
    """ Delete receiver from DB """
    command = "DELETE FROM receivers WHERE station_id=?"
    DATABASE_EDIT_Q.put(
        (command, [(station_id,), ], True))
    DATABASE_RETURN.get(timeout=1)
    DATABASE_EDIT_Q.put(("done", None, False))


# noinspection SqlDialectInspection
def update_rx_table():
    """ Updates the database with any changes made to the receivers. """
    for item in Context.receiver_controller.receivers:
        rx = item.receiver_dict()
        to_table = [rx['auto'], rx['mobile'], rx['single'],
                    rx['latitude'], rx['longitude'], rx['stationId']]
        command = """UPDATE receivers SET isAuto=?,
                                          isMobile=?,
                                          isSingle=?,
                                          latitude=?,
                                          longitude=?
                     WHERE station_id = ?"""
        DATABASE_EDIT_Q.put((command, [to_table, ], True))
        # try:
        DATABASE_RETURN.get(timeout=1)
        # except:
        #     pass
    DATABASE_EDIT_Q.put(("done", None, False))


# noinspection SqlDialectInspection
def add_aoi(aoi_type, lat, lon, radius):
    """ Updates the database with new interest areas. """
    conn = sqlite3.connect(database_name)
    c = conn.cursor()

    prev_uid = c.execute('SELECT MAX(uid) from interest_areas').fetchone()[0]
    conn.close()
    uid = (prev_uid + 1) if prev_uid is not None else 0
    to_table = [uid, aoi_type, lat, lon, radius]
    command = 'INSERT INTO interest_areas VALUES (?,?,?,?,?)'
    DATABASE_EDIT_Q.put((command, [to_table, ], True))
    DATABASE_RETURN.get(timeout=1)
    DATABASE_EDIT_Q.put(("done", None, False))


def fetch_aoi_data():
    """ Read all the AOIs from the DB """
    conn = sqlite3.connect(database_name)
    c = conn.cursor()
    c.execute('SELECT * FROM interest_areas')
    aoi_list = c.fetchall()
    conn.close()
    return aoi_list


def stop_server(server):
    """ Stop server and execute finish gracefuly """

    def wr(_signum, _frame):
        """ We need a wraper to dynamicaly set server """
        update_rx_table()

        server.stop()
        # server.join()

        if geofile is not None:  # OLEKSA, FIX THIS PLEASE ASAP !!!
            write_geojson(*process_data(database_name)[:2])

    return wr


if __name__ == '__main__':
    ###############################################
    # Help info printed when calling the program
    ###############################################
    usage = "usage: %prog -d FILE [options]"
    parser = OptionParser(usage=usage)
    parser.add_option("-d", "--database", dest="database_name",
                      help="REQUIRED Database File", metavar="FILE")
    parser.add_option("-r", "--receivers", dest="rx_file",
                      help="List of receiver URLs", metavar="FILE")
    parser.add_option("-g", "--geofile", dest="geofile",
                      help="GeoJSON Output File", metavar="FILE")
    parser.add_option("-e", "--epsilon", dest="eps",
                      help="Max Clustering Distance, Default \"auto\".",
                      metavar="NUMBER or \"auto\"", default="auto")
    parser.add_option("-c", "--confidence", dest="conf",
                      help="Minimum confidence value, default 10",
                      metavar="NUMBER", type="int", default=10)
    parser.add_option("-p", "--power", dest="pwr",
                      help="Minimum power value, default 10",
                      metavar="NUMBER", type="int", default=10)
    parser.add_option("-m", "--min-samples", dest="minsamp",
                      help="Minimum samples per cluster. Default: \"auto\"",
                      metavar="NUMBER or \"auto\"", default="auto")
    parser.add_option("--plot_intersects", dest="plotintersects",
                      help=("Plots all the intersect points in a cluster. "
                            "Only applies when clustering is turned on. "
                            "This creates larger CZML files."),
                      action="store_true")
    parser.add_option("-o", "--offline", dest="disable",
                      help="Starts program with receiver turned off.",
                      action="store_false", default=True)
    parser.add_option("--access_token", dest="token_file",
                      help="Cesium Access Token File", metavar="FILE")
    parser.add_option("--ip", dest="ipaddr",
                      help="IP Address to serve from. Default 127.0.0.1",
                      metavar="IP ADDRESS", type="str", default="127.0.0.1")
    parser.add_option("--port", dest="port",
                      help="Port number to serve from. Default 8080",
                      metavar="NUMBER", type="int", default=8080)
    parser.add_option("--debug", dest="debugging",
                      help="Does not clear the screen. "
                           "Useful for seeing errors and warnings.",
                      action="store_true")
    (options, args) = parser.parse_args()

    # INIT LOGGING
    logging_level = logging.DEBUG
    if not options.debugging:  # Limit some spam
        # noinspection SpellCheckingInspection
        logging.getLogger("geventwebsocket").level = logging.WARNING
        logging.getLogger("asyncio").level = logging.INFO
        logging_level = logging.INFO
    init_logging(level=logging_level)
    #

    mandatories = ['database_name']
    for m in mandatories:
        if options.__dict__[m] is None:
            logger.error("You forgot an arguement")
            parser.print_help()
            exit(-1)

    ms = math_settings(options.eps, options.minsamp, options.conf, options.pwr)

    geofile = options.geofile
    rx_file = options.rx_file
    database_name = options.database_name
    debugging = False if not options.debugging else True
    ms.receiving = options.disable
    ms.plotintersects = options.plotintersects

    if options.token_file:
        tokenfile = options.token_file
        with open(tokenfile, "r") as token:
            access_token = token.read().replace('\n', '')
        # print(access_token)
    else:
        access_token = None

    web = BottleWebServer(options.ipaddr, options.port)
    web.start()

    db_writer = DatabaseWriter(database_name, DATABASE_EDIT_Q, DATABASE_RETURN)
    db_writer.start()

    ###############################################
    # Run the main loop!
    ###############################################
    # while True:
    #     if ms.receiving:
    #         run_receiver(receivers)
    #     if not debugging:
    #         print("Receiver Paused")
    #     # 11.01.2023::s1z: I don't know wtf is this crap,
    #     #                  but sleep 1 sec,
    #     #                  You gotta be kidding me.
    #     #                  Changed from 1 sec to 10 ms.
    #     time.sleep(0.001)

    io_loop = asyncio.get_event_loop()
    receiver_controller = ReceiverController(database_name, rx_file)
    receiver_controller.set_epsilon(ms.eps)
    receiver_controller.set_sampling(ms.min_samp)
    receiver_controller.set_confidence(ms.min_conf)
    receiver_controller.load_receivers()
    receiver_controller.start()

    ws_client = WSClient(io_loop=io_loop, scheme="https", host="cds.s1z.info",
                         path="/api/v1/ws-agg",
                         user="kraken", password="kraken", ca_file="./ca.pem",
                         on_data_handler=receiver_controller.handle_data)
    ws_client.start()
    Context.db_writer = db_writer
    Context.ws_client = ws_client
    Context.receiver_controller = receiver_controller
    ether_service = EtherService(web, db_writer, shared_dict, ws_client,
                                 receiver_controller)
    signal.signal(signal.SIGINT, stop_server(ether_service))
    signal.signal(signal.SIGTERM, stop_server(ether_service))
    ether_service.start()
    ether_service.join()
