# Martin Konečnik, http://git.siwim.si/mkonecnik/integration-ukraine
# Script contains helper functions used by both DSBT and UAD.
import sys
from datetime import datetime
from logging import getLogger
from pathlib import Path
from typing import Optional, Union

from lxml import etree

logger = getLogger('main')


def convert_kN_to_kg(value: Union[float, str]) -> int:
    return round(50 * (float(value) * 101.97 // 50))  # Round to 50.


def convert_m_to_mm(value: str) -> int:
    return round(float(value) * 1000)


def convert_m_per_s_to_km_per_h(value: str) -> int:
    return round(float(value) * 3.6)


def convert_ts(ts: str, timezone: str) -> str:
    return f'{datetime.strptime(ts, "%Y-%m-%d-%H-%M-%S-%f").strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]}+{timezone}'


def classify_ua(cls: str) -> int:
    if cls != '140':
        if cls in ua20_classes:
            return ua20_classes[cls]
        else:
            logger.error(f'Ukrainian classification for {cls} does not exist in {cypher_file}. Class handled as unclassified!')
            return 20
    else:
        return 20


def classify_eur6(cls: str) -> int:
    if cls != '140':
        if cls in eur6_classes:
            return eur6_classes[cls]
        else:
            logger.error(f'EUR6 classification for {cls} does not exist in {cypher_file}. Class handled as unclassified!')
            return 7
    else:
        return 7  # This isn't a classification, but since we're just using this to decide whether to ditch the vehicle (above or below class 3), it'll work.


def check_flag(flags: str, index: int, check_for: int) -> bool:
    flag = int(flags[index], 16)
    values = [8, 4, 2, 1]
    for val in values:
        if flag >= val:
            flag -= val
            if val == check_for:
                return True
    return False


def get_cameras_from_i_conf(conf_path: str = r'D:\siwim_mkiii\siwim_i\conf\conf.xml', cam_root: Path = Path('ext')) -> Optional[dict]:
    try:
        conf: etree.Element = etree.parse(conf_path)
    except:
        logger.critical(f'Failed to read SiWIM-I conf. Make sure that it exists at {conf_path}.')
        sys.exit()

    # There's three types of cameras: Frontal, Overview and Plate.
    plate_cameras = [el.text for el in conf.xpath('modules/module[@type="aggr"]/downstream_modules/downstream_module[starts-with(text(), "camera")]')]  # Get all cameras that are downstream from aggregation.
    other_cameras = [el.text for el in conf.xpath('modules/module[@type="rcv"]/downstream_modules/downstream_module[starts-with(text(), "camera")]')]  # Get all the other cameras.

    cameras_dict = dict()  # This will be a dict of all the cameras.
    for cam in plate_cameras:
        cam_plate = conf.xpath(f'modules/module[@type="cam"]/name[text()="{cam}"]/parent::*')[0]  # Find the correct module.
        for lane in cam_plate.xpath('lanes/lane'):
            cameras_dict[f'plate_{lane.text}'] = cam_root.joinpath('anpr', cam_plate.find('init_type').text, cam_plate.find('name').text)

        # Find the camera module with the same IP as plate knowing this is the frontal cam_plate for same lane(s).
        cams_by_ip = conf.xpath(f'modules/module[@type="cam"]/host[text()="{cam_plate.find("host").text}"]/parent::*')
        for c in cams_by_ip:
            if c.find('init_type').text != cam_plate.find('init_type').text:
                for ln in c.xpath('lanes/lane'):
                    cameras_dict[f'front_{ln.text}'] = cam_root.joinpath('photo', c.find('init_type').text, c.find('name').text)
                other_cameras.remove(c.find('name').text)  # Remove this camera from other_cameras list as it was handled here.
                break

    for cam in other_cameras:  # Should only contain overview camera names now.
        cam_over = conf.xpath(f'modules/module[@type="cam"]/name[text()="{cam}"]/parent::*')[0]
        for ln in cam_over.xpath('lanes/lane'):
            cameras_dict[f'side_{ln.text}'] = cam_root.joinpath('photo', cam_over.find('init_type').text, cam_over.find('name').text)

    return cameras_dict


def get_photo_ts(path: str, timezone: str) -> str:
    with open(path, 'rb') as fd_img:  # Read the image to find timestamp.
        img = fd_img.read()
    ts_start = img.find(b'SiWIM_PHOTO_TS:') + len(b'SiWIM_PHOTO_TS:')
    return convert_ts(img[ts_start:ts_start + 23].decode(), timezone)


def are_images_present(img_dict: dict, ts: str) -> None:
    for typ, val in img_dict.items():
        if val is None:
            getLogger('info').warning(f'{ts} has no image for {typ}.')


def to_hex(string: bytes):
    return '{:x}'.format(int.from_bytes(string, byteorder='big')).rjust(64, '0')


eur6_classes = dict()  # Classification under eur6 specs.
ua20_classes = dict()  # Classification under ua specs.

# Determine the filename. We assume format will stay the same, but newer versions will have a higher version number. Since this is imported from main program, the path will reflect that.
ver = 1
cypher_files = list()
file_root = 'ua_sifrant'  # Root of the cypher file. Upper/lowercase doesn't matter.
for filename in Path('..').glob('*'):
    if filename.name[:len(file_root)].lower() == file_root:  # Make a list of all cyphers on the system.
        cypher_files.append(filename)
cypher_file = sorted(cypher_files, reverse=True)[0]  # This won't work if moving from v 9 -> 10, but enough of solving non-existent problems.
with Path(cypher_file).open('r') as cypher:
    lines = cypher.readlines()

# Read the conf and save reclassification values.
for line in lines:
    cls, eur6, ua20 = line.strip().split(';')
    eur6_classes[cls] = int(eur6)
    ua20_classes[cls] = int(ua20)
