__filename__ = "city.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.5.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Metadata"

import os
import datetime
import random
import math
from random import randint
from utils import acct_dir
from utils import remove_eol

# states which the simulated city dweller can be in
PERSON_SLEEP = 0
PERSON_WORK = 1
PERSON_PLAY = 2
PERSON_SHOP = 3
PERSON_EVENING = 4
PERSON_PARTY = 5

BUSY_STATES = (PERSON_WORK, PERSON_SHOP, PERSON_PLAY, PERSON_PARTY)


def _get_decoy_camera(decoy_seed: int) -> (str, str, int):
    """Returns a decoy camera make and model which took the photo
    """
    cameras = [
        ["Apple", "iPhone SE"],
        ["Apple", "iPhone XR"],
        ["Apple", "iPhone 7"],
        ["Apple", "iPhone 8"],
        ["Apple", "iPhone 11"],
        ["Apple", "iPhone 11 Pro"],
        ["Apple", "iPhone 12"],
        ["Apple", "iPhone 12 Mini"],
        ["Apple", "iPhone 12 Pro Max"],
        ["Apple", "iPhone 13"],
        ["Apple", "iPhone 13 Mini"],
        ["Apple", "iPhone 13 Pro"],
        ["Apple", "iPhone 14"],
        ["Apple", "iPhone 14 Pro"],
        ["Samsung", "Galaxy Note 20 Ultra"],
        ["Samsung", "Galaxy S20 Plus"],
        ["Samsung", "Galaxy S20 FE 5G"],
        ["Samsung", "Galaxy Z FOLD 2"],
        ["Samsung", "Galaxy S12 Plus"],
        ["Samsung", "Galaxy S12"],
        ["Samsung", "Galaxy S11 Plus"],
        ["Samsung", "Galaxy S10 Plus"],
        ["Samsung", "Galaxy S10e"],
        ["Samsung", "Galaxy Z Flip"],
        ["Samsung", "Galaxy A54"],
        ["Samsung", "Galaxy A51"],
        ["Samsung", "Galaxy S10"],
        ["Samsung", "Galaxy S10 Plus"],
        ["Samsung", "Galaxy S10e"],
        ["Samsung", "Galaxy S10 5G"],
        ["Samsung", "Galaxy A60"],
        ["Samsung", "Note 13"],
        ["Samsung", "Note 13 Plus"],
        ["Samsung", "Note 12"],
        ["Samsung", "Note 12 Plus"],
        ["Samsung", "Note 11"],
        ["Samsung", "Note 11 Plus"],
        ["Samsung", "Note 10"],
        ["Samsung", "Note 10 Plus"],
        ["Samsung", "Galaxy S23 Ultra"],
        ["Samsung", "Galaxy S23"],
        ["Samsung", "Galaxy S22 Ultra"],
        ["Samsung", "Galaxy S22"],
        ["Samsung", "Galaxy S21 Ultra"],
        ["Samsung", "Galaxy Note 20 Ultra"],
        ["Samsung", "Galaxy S21"],
        ["Samsung", "Galaxy S21 Plus"],
        ["Samsung", "Galaxy S20 FE"],
        ["Samsung", "Galaxy Z Fold 2"],
        ["Samsung", "Galaxy A52 5G"],
        ["Samsung", "Galaxy A71 5G"],
        ["Google", "Pixel 7 Pro"],
        ["Google", "Pixel 7"],
        ["Google", "Pixel 6 Pro"],
        ["Google", "Pixel 6"],
        ["Google", "Pixel 5"],
        ["Google", "Pixel 4a"],
        ["Google", "Pixel 4 XL"],
        ["Google", "Pixel 3 XL"],
        ["Google", "Pixel 4"],
        ["Google", "Pixel 4a 5G"],
        ["Google", "Pixel 3"],
        ["Google", "Pixel 3a"]
    ]
    randgen = random.Random(decoy_seed)
    index = randgen.randint(0, len(cameras) - 1)
    serial_number = randgen.randint(100000000000, 999999999999999999999999)
    return cameras[index][0], cameras[index][1], serial_number


def _get_city_pulse(curr_time_of_day, decoy_seed: int) -> (float, float):
    """This simulates expected average patterns of movement in a city.
    Jane or Joe average lives and works in the city, commuting in
    and out of the central district for work. They have a unique
    life pattern, which machine learning can latch onto.
    This returns a polar coordinate for the simulated city dweller:
    Distance from the city centre is in the range 0.0 - 1.0
    Angle is in radians
    """
    randgen = random.Random(decoy_seed)
    variance = 3
    data_decoy_state = PERSON_SLEEP
    weekday = curr_time_of_day.weekday()
    min_hour = 7 + randint(0, variance)
    max_hour = 17 + randint(0, variance)
    if curr_time_of_day.hour > min_hour:
        if curr_time_of_day.hour <= max_hour:
            if weekday < 5:
                data_decoy_state = PERSON_WORK
            elif weekday == 5:
                data_decoy_state = PERSON_SHOP
            else:
                data_decoy_state = PERSON_PLAY
        else:
            if weekday < 5:
                data_decoy_state = PERSON_EVENING
            else:
                data_decoy_state = PERSON_PARTY
    randgen2 = random.Random(decoy_seed + data_decoy_state)
    angle_radians = \
        (randgen2.randint(0, 100000) / 100000) * 2 * math.pi
    # some people are quite random, others have more predictable habits
    decoy_randomness = randgen.randint(1, 3)
    # occasionally throw in a wildcard to keep the machine learning guessing
    if randint(0, 100) < decoy_randomness:
        distance_from_city_center = (randint(0, 100000) / 100000)
        angle_radians = (randint(0, 100000) / 100000) * 2 * math.pi
    else:
        # what consitutes the central district is fuzzy
        central_district_fuzz = (randgen.randint(0, 100000) / 100000) * 0.1
        busy_radius = 0.3 + central_district_fuzz
        if data_decoy_state in BUSY_STATES:
            # if we are busy then we're somewhere in the city center
            distance_from_city_center = \
                (randgen.randint(0, 100000) / 100000) * busy_radius
        else:
            # otherwise we're in the burbs
            distance_from_city_center = busy_radius + \
                ((1.0 - busy_radius) * (randgen.randint(0, 100000) / 100000))
    return distance_from_city_center, angle_radians


def parse_nogo_string(nogo_line: str) -> []:
    """Parses a line from locations_nogo.txt and returns the polygon
    """
    nogo_line = remove_eol(nogo_line)
    polygon_str = nogo_line.split(':', 1)[1]
    if ';' in polygon_str:
        pts = polygon_str.split(';')
    else:
        pts = polygon_str.split(',')
    if len(pts) <= 4:
        return []
    polygon = []
    for index in range(int(len(pts)/2)):
        if index*2 + 1 >= len(pts):
            break
        longitude_str = pts[index*2].strip()
        latitude_str = pts[index*2 + 1].strip()
        if 'E' in latitude_str or 'W' in latitude_str:
            longitude_str = pts[index*2 + 1].strip()
            latitude_str = pts[index*2].strip()
        if 'E' in longitude_str:
            longitude_str = \
                longitude_str.replace('E', '')
            longitude = float(longitude_str)
        elif 'W' in longitude_str:
            longitude_str = \
                longitude_str.replace('W', '')
            longitude = -float(longitude_str)
        else:
            longitude = float(longitude_str)
        latitude = float(latitude_str)
        polygon.append([latitude, longitude])
    return polygon


def spoof_geolocation(base_dir: str,
                      city: str, curr_time, decoy_seed: int,
                      cities_list: [],
                      nogo_list: []) -> (float, float, str, str,
                                         str, str, int):
    """Given a city and the current time spoofs the location
    for an image
    returns latitude, longitude, N/S, E/W,
    camera make, camera model, camera serial number
    """
    locations_filename = base_dir + '/custom_locations.txt'
    if not os.path.isfile(locations_filename):
        locations_filename = base_dir + '/locations.txt'

    nogo_filename = base_dir + '/custom_locations_nogo.txt'
    if not os.path.isfile(nogo_filename):
        nogo_filename = base_dir + '/locations_nogo.txt'

    man_city_radius = 0.1
    variance_at_location = 0.0004
    default_latitude = 51.8744
    default_longitude = 0.368333
    default_latdirection = 'N'
    default_longdirection = 'W'

    if cities_list:
        cities = cities_list
    else:
        if not os.path.isfile(locations_filename):
            return (default_latitude, default_longitude,
                    default_latdirection, default_longdirection,
                    "", "", 0)
        cities = []
        try:
            with open(locations_filename, 'r', encoding='utf-8') as loc_file:
                cities = loc_file.readlines()
        except OSError:
            print('EX: unable to read locations ' + locations_filename)

    nogo = []
    if nogo_list:
        nogo = nogo_list
    else:
        if os.path.isfile(nogo_filename):
            nogo_list = []
            try:
                with open(nogo_filename, 'r', encoding='utf-8') as nogo_file:
                    nogo_list = nogo_file.readlines()
            except OSError:
                print('EX: unable to read ' + nogo_filename)
            for line in nogo_list:
                if line.startswith(city + ':'):
                    polygon = parse_nogo_string(line)
                    if polygon:
                        nogo.append(polygon)

    city = city.lower()
    for city_name in cities:
        if city in city_name.lower():
            city_fields = city_name.split(':')
            latitude = city_fields[1]
            longitude = city_fields[2]
            area_km2 = 0
            if len(city_fields) > 3:
                area_km2 = int(city_fields[3])
            latdirection = 'N'
            longdirection = 'E'
            if 'S' in latitude:
                latdirection = 'S'
                latitude = latitude.replace('S', '')
            if 'W' in longitude:
                longdirection = 'W'
                longitude = longitude.replace('W', '')
            latitude = float(latitude)
            longitude = float(longitude)
            # get the time of day at the city
            approx_time_zone = int(longitude / 15.0)
            if longdirection == 'E':
                approx_time_zone = -approx_time_zone
            curr_time_adjusted = curr_time - \
                datetime.timedelta(hours=approx_time_zone)
            cam_make, cam_model, cam_serial_number = \
                _get_decoy_camera(decoy_seed)
            valid_coord = False
            seed_offset = 0
            while not valid_coord:
                # patterns of activity change in the city over time
                (distance_from_city_center, angle_radians) = \
                    _get_city_pulse(curr_time_adjusted,
                                    decoy_seed + seed_offset)
                # The city radius value is in longitude and the reference
                # is Manchester. Adjust for the radius of the chosen city.
                if area_km2 > 1:
                    man_radius = math.sqrt(1276 / math.pi)
                    radius = math.sqrt(area_km2 / math.pi)
                    city_radius_deg = (radius / man_radius) * man_city_radius
                else:
                    city_radius_deg = man_city_radius
                # Get the position within the city, with some randomness added
                latitude += \
                    distance_from_city_center * city_radius_deg * \
                    math.cos(angle_radians)
                longitude += \
                    distance_from_city_center * city_radius_deg * \
                    math.sin(angle_radians)
                longval = longitude
                if longdirection == 'W':
                    longval = -longitude
                valid_coord = not point_in_nogo(nogo, latitude, longval)
                if not valid_coord:
                    seed_offset += 1
                    if seed_offset > 100:
                        break
            # add a small amount of variance around the location
            fraction = randint(0, 100000) / 100000
            distance_from_location = fraction * fraction * variance_at_location
            fraction = randint(0, 100000) / 100000
            angle_from_location = fraction * 2 * math.pi
            latitude += distance_from_location * math.cos(angle_from_location)
            longitude += distance_from_location * math.sin(angle_from_location)

            # gps locations aren't transcendental, so round to a fixed
            # number of decimal places
            latitude = int(latitude * 100000) / 100000.0
            longitude = int(longitude * 100000) / 100000.0
            return (latitude, longitude, latdirection, longdirection,
                    cam_make, cam_model, cam_serial_number)

    return (default_latitude, default_longitude,
            default_latdirection, default_longdirection,
            "", "", 0)


def get_spoofed_city(city: str, base_dir: str,
                     nickname: str, domain: str) -> str:
    """Returns the name of the city to use as a GPS spoofing location for
    image metadata
    """
    city = ''
    city_filename = acct_dir(base_dir, nickname, domain) + '/city.txt'
    if os.path.isfile(city_filename):
        try:
            with open(city_filename, 'r', encoding='utf-8') as city_file:
                city1 = city_file.read()
                city = remove_eol(city1)
        except OSError:
            print('EX: unable to read ' + city_filename)
    return city


def _point_in_polygon(poly: [], x_coord: float, y_coord: float) -> bool:
    """Returns true if the given point is inside the given polygon
    """
    num = len(poly)
    inside = False
    p2x = 0.0
    p2y = 0.0
    xints = 0.0
    p1x, p1y = poly[0]
    for i in range(num + 1):
        p2x, p2y = poly[i % num]
        if y_coord > min(p1y, p2y):
            if y_coord <= max(p1y, p2y):
                if x_coord <= max(p1x, p2x):
                    if p1y != p2y:
                        xints = \
                            (y_coord - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                    if p1x == p2x or x_coord <= xints:
                        inside = not inside
        p1x, p1y = p2x, p2y

    return inside


def point_in_nogo(nogo: [], latitude: float, longitude: float) -> bool:
    """Returns true of the given geolocation is within a nogo area
    """
    for polygon in nogo:
        if _point_in_polygon(polygon, latitude, longitude):
            return True
    return False