from datetime import datetime
from enum import IntEnum

import urllib.request

import cv2 as cv
import numpy as np


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @staticmethod
    def from_dict(dictionary):
        return Point(dictionary['x'], dictionary['y'])

    def to_dict(self):
        return {'x': self.x, 'y': self.y}

    def __repr__(self):
        return f'({self.x}, {self.y})'


class Rotation(IntEnum):
    CW_0 = 0
    CW_90 = 90
    CW_180 = 180
    CW_270 = 270
    CCW_0 = CW_0
    CCW_90 = CW_270
    CCW_180 = CW_180
    CCW_270 = CW_90


class Orientation:
    def __init__(self, flip, rotation):
        self.flip = flip
        self.rotation = rotation

    def to_exif(self):
        if self.flip:
            if self.rotation == Rotation.CW_0:
                return 2
            elif self.rotation == Rotation.CW_180:
                return 4
            elif self.rotation == Rotation.CW_90:
                return 5
            elif self.rotation == Rotation.CW_270:
                return 7
        else:
            if self.rotation == Rotation.CW_0:
                return 1
            elif self.rotation == Rotation.CW_180:
                return 3
            elif self.rotation == Rotation.CW_90:
                return 6
            elif self.rotation == Rotation.CW_270:
                return 8

    @staticmethod
    def from_exif(exif_value):
        return Orientation.values[exif_value]

    @staticmethod
    def default():
        return Orientation(False, Rotation.CW_0)

    def __repr__(self):
        return f'{self.flip}:{self.rotation}'


Orientation.values = {
    1: Orientation(False, Rotation.CW_0),
    2: Orientation(True, Rotation.CW_0),
    3: Orientation(False, Rotation.CW_180),
    4: Orientation(True, Rotation.CW_180),
    5: Orientation(True, Rotation.CW_90),
    6: Orientation(False, Rotation.CW_90),
    7: Orientation(True, Rotation.CW_270),
    8: Orientation(False, Rotation.CW_270),
}


class Size(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def __iter__(self):
        yield from (self.width, self.height)

    def rotate90cw(self):
        return Size(self.height, self.width)

    def rotate180cw(self):
        return Size(self.width, self.height)

    def rotate270cw(self):
        return Size(self.height, self.width)

    def transform(self, orientation):
        assert not orientation.flip

        if orientation.rotation == Rotation.CW_90:
            return self.rotate90cw()
        elif orientation.rotation == Rotation.CW_180:
            return self.rotate180cw()
        elif orientation.rotation == Rotation.CW_270:
            return self.rotate270cw()
        else:
            return Size(self.width, self.height)


class Feature(object):
    def __init__(self, data):
        self.id = data['feature_id']
        self.source_id = data['source_id']
        self.heading = data['heading']
        self.timestamp = data['date']
        self.mds_group_id = data['mds_group_id']
        self.mds_path = data['mds_path']
        self.width = data['width']
        self.height = data['height']
        self.quality = data['quality']
        self.road_probability = data['road_probability']
        self.forbidden_probability = data['forbidden_probability']
        self.dataset = data['dataset']
        self.is_published = data['is_published']
        self.should_be_published = data['is_published']
        self.camera_deviation = data['camera_deviation']
        self.privacy = data['privacy']
        self.uploaded_at = data['uploaded_at']
        self.graph = data['graph']

        pos = data['pos']
        assert pos is not None
        self.pos = Point.from_dict(pos)

        orientation = data['orientation']
        self.orientation = None if orientation is None else Orientation.from_exif(orientation)

        odometer_pos = data['odometer_pos']
        self.odometer_pos = None if odometer_pos is None else Point.from_dict(odometer_pos)

        camera_orientation_rodrigues = data['camera_orientation_rodrigues']
        self.camera_orientation_rodrigues = (
            None if camera_orientation_rodrigues is None else np.array(camera_orientation_rodrigues)
        )

    def original_size(self):
        return Size(self.width, self.height)

    def size(self):
        return self.original_size().transform(self.orientation)

    def date(self):
        return datetime.fromtimestamp(self.timestamp / 10 ** 9)

    def to_dict(self):
        return {
            'feature_id': self.id,
            'source_id': self.source_id,
            'heading': self.heading,
            'date': self.timestamp,
            'mds_group_id': self.mds_group_id,
            'mds_path': self.mds_path,
            'width': self.width,
            'height': self.height,
            'quality': self.quality,
            'road_probability': self.road_probability,
            'forbidden_probability': self.forbidden_probability,
            'dataset': self.dataset,
            'is_published': self.is_published,
            'should_be_published': self.should_be_published,
            'camera_deviation': self.camera_deviation,
            'privacy': self.privacy,
            'uploaded_at': self.uploaded_at,
            'graph': self.graph,
            'pos': self.pos.to_dict(),
            'orientation': self.orientation.to_exif() if self.orientation else None,
            'odometer_pos': self.odometer_pos.to_dict() if self.odometer_pos else None,
            'camera_orientation_rodrigues': None
            if self.camera_orientation_rodrigues is None
            else self.camera_orientation_rodrigues.tolist(),
        }


def exif_transform(image, orientation):
    assert not orientation.flip

    if orientation.rotation == Rotation.CW_90:
        return cv.rotate(image, cv.ROTATE_90_CLOCKWISE)
    elif orientation.rotation == Rotation.CW_180:
        return cv.rotate(image, cv.ROTATE_180)
    elif orientation.rotation == Rotation.CW_270:
        return cv.rotate(image, cv.ROTATE_90_COUNTERCLOCKWISE)
    else:
        return image


class MdsLoader:
    def __init__(self, mdsHost, port=80):
        self.host_ = mdsHost
        self.port_ = port
        self.namespace_ = 'maps_mrc'

    def make_url(self, feature):
        return "http://{host}:{port}/get-{namespace}/{group_id}/{path}".format(
            host=self.host_,
            port=self.port_,
            namespace=self.namespace_,
            group_id=feature.mds_group_id,
            path=feature.mds_path,
        )

    def __call__(self, feature, transform=True):
        respond = urllib.request.urlopen(self.make_url(feature))
        data = np.asarray(bytearray(respond.read()), dtype=np.uint8)
        image = cv.imdecode(data, cv.IMREAD_COLOR | cv.IMREAD_IGNORE_ORIENTATION)

        if transform:
            assert feature.orientation is not None
            image = exif_transform(image, feature.orientation)

        return image
