import json
import requests
import pickle
import urllib.parse

from bridges.data_src_dependent import earthquake_usgs
from bridges.data_src_dependent import actor_movie_imdb
from bridges.data_src_dependent import game
from bridges.data_src_dependent import shakespeare
from bridges.data_src_dependent import gutenberg_book
from bridges.data_src_dependent import gutenberg_meta
from bridges.data_src_dependent import cancer_incidence
from bridges.data_src_dependent import song
from bridges.data_src_dependent import lru_cache
from bridges.data_src_dependent import movie_actor_wiki_data
from bridges.data_src_dependent.osm import *
from bridges.data_src_dependent.elevation import *
from bridges.data_src_dependent.amenity import *
from bridges.data_src_dependent.actor_movie_imdb import *
from bridges.color_grid import ColorGrid
from bridges.color import Color
from SPARQLWrapper import SPARQLWrapper, JSON



def get_game_data():
    """
    @brief Get meta data of the IGN games collection.

    This function retrieves  and formats the data into a list of
    Game objects

    @throws Exception if the request fails
    
    @return a list of Game objects,
    """

    wrapper = []

    url = "http://bridgesdata.herokuapp.com/api/games"
    PARAMS = {"Accept: application/json"}

    r = requests.get(url=url, params=str(PARAMS))

    r = r.json()

    D = r["data"]
    # print(D)

    for i in range(len(D)):
        V = D[i]
        G = V["genre"]
        genre = []
        for j in range(len(G)):
            genre.append(str(G[j]))
        wrapper.append(game.Game(V["game"], V["platform"], V["rating"], str(genre)))
    return wrapper


def parse_actor_movie_imdb(item):
    """
    @brief Parses an actor movie pair
    Args:
        item: input actor-movie pair
    """
    am_pair = ActorMovieIMDB()
    am_pair.actor = item["actor"]
    am_pair.movie = item["movie"]
    return am_pair


def get_actor_movie_imdb_data(count = 0):
    """
    @brief Get ActorMovie IMDB Data. 

    Data is retrieved, formatted into a list of ActorMovieIMDB objects

    Args:
        count: count of the number of actor/movie pairs, but currently unused,
        returns all records.

    Returns:
        a list of ActorMovieIMDB objects, but only actor and movie fields in this version
    @throws Exception if the request fails
    """

    wrapper = []

    url = "http://bridgesdata.herokuapp.com/api/imdb?limit=" + str(count)
    PARAMS = {"Accept: application/json"}

    r = requests.get(url=url, params=str(PARAMS))
    # print(r.status_code, r.reason)

    data = r.json()

    D = data["data"]

    for i in range(len(D)):
        V = D[i]
        wrapper.append(actor_movie_imdb.ActorMovieIMDB(V["actor"], V["movie"]))

    return wrapper

def get_actor_movie_imdb_data2():
    """
    Get ActorMovie IMDB Data. Data is retrieved, formatted into a list of ActorMovieIMDB objects
    Args:
        count: the number of actor/movie pairs, but currently unused,
    Returns:
        All records.
    Throws: 
        Exception if the request fails
    """
    url = "https://bridgesdata.herokuapp.com/api/imdb2"

    r = requests.get(url = url)
    status = r.status_code

    data = r.json()
    D = data["data"]
    am_list = []

    if status == 200:
        for i in range(len(D)):
            V = D[i]
            am_pair = parse_actor_movie_imdb(V)
            am_pair.rating = int(V['rating'])

            genre = V['genres']
            v = []
            for k in range(len(genre)):
                v.append(genre[k])
            am_pair._genres = v
            am_list.append(am_pair)
        return am_list
    else:
        r.raise_for_status()


def get_earthquake_usgs_data(count = 0):
    """
    @brief Get USGS earthquake data

    USGS Tweet data (https://earthquake.usgs.gov/earthquakes/map/). Data
    retrieved, formatted into a list of EarthquakeUSGS objects

    Args:
        count: the number of earthquake records retrieved

    @throws Exception if the request fails

    Returns:
        a list of earthquake records
    """
    wrapper = []
    url = "http://earthquakes-uncc.herokuapp.com/eq"
    latest_url = "http://earthquakes-uncc.herokuapp.com/eq/latest/" + str(count)
    PARAMS = {"Accept: application/json"}

    if count <= 0:
        r = requests.get(url=url, params=str(PARAMS))
        r = r.json()
        for i in range(len(r)):
            V = r[i]["properties"]
            G = r[i]["geometry"]["coordinates"]
            wrapper.append(earthquake_usgs.EarthquakeUSGS(V["mag"], G[0], G[1], V["place"], V["title"], V["url"], V["time"]))
    else:
        r = requests.get(url=latest_url, params=str(PARAMS))
        data = r.json()
        D = data["Earthquakes"]
        for i in range(len(D)):
            V = D[i]["properties"]
            G = D[i]["geometry"]["coordinates"]
            wrapper.append(earthquake_usgs.EarthquakeUSGS(V["mag"], G[0], G[1], V["place"], V["title"], V["url"], V["time"]))
    return wrapper


def get_shakespeare_data(endpoint = "", textonly = False):
    """
    @brief Get data of Shakespeare works (plays, poems)

    This function retrieves  and formats the data into a
    a list of Shakespeare objects.

    Valid endpoints: 'poems','plays'
    Valid queryParams: format{simple}

    @throws Exception if the request fails

    Args: 
        endpoint:  can be either "plays" or "poems". If this is
            specified, then only these types of works are retrieved.
        textonly:  if this is set, then only the text is retrieved.

    Returns:
        An array of Shakespeare objects
    """

    wrapper = []
    url = "http://bridgesdata.herokuapp.com/api/shakespeare/"
    PARAMS = {"Accept: application/json"}

    if endpoint == "plays" or endpoint == "poems":
        url += "/" + endpoint
    if textonly:
        url += "?format=simple"

    r = requests.get(url = url, params = str(PARAMS))
    r = r.json()

    D = r["data"]
    for i in range(len(D)):
        V = D[i]
        wrapper.append(shakespeare.Shakespeare(V["title"], V["type"], V["text"]))
    return wrapper


def get_gutenberg_book_data(num = 0):

    """
    @brief Get meta data of the Gutenberg book collection (1000 books)

    This function retrieves,  and formats the data into a list of
    GutenbergBook objects

    @throws Exception if the request fails

    Returns:
        A list of GutenbergBook objects,
    """
    wrapper = []
    url = "http://bridgesdata.herokuapp.com/api/books"
    PARAMS = {"Accept: application/json"}

    if num > 0:
        url += "?limit=" + str(num)

    r = requests.get(url=url, params = str(PARAMS))
    r = r.json()

    D = r["data"]
    for i in range(len(D)):
        V = D[i]

        A = V["author"]
        L = V["languages"]

        lang = []

        for j in range(len(L)):
            lang.append(str(L[j]))

        G = V["genres"]
        genre = []
        for j in range(len(G)):
            genre.append(str(G[j]))

        S = V["subjects"]
        subject = []
        for j in range(len(S)):
            subject.append(str(S[j]))

        M = V["metrics"]
        wrapper.append(gutenberg_book.GutenbergBook(A["name"], A["birth"], A["death"], V["title"],
                                                    str(lang), str(genre), str(subject), M["characters"], M["words"],
                                                    M["sentences"], M["difficultWords"], V["url"], V["downloads"]))

    return wrapper


def get_cancer_incident_data(num = 0):
    """
    @brief Retrieves the CDC cancer incidence dataset.

    Returns a list of records.  See CancerIncidence class for more information

    Args:
        num:  count of records to be retrieved
    Returns:
         cancer incidence data records in a list
    """
    wrapper = []
    url = "https://bridgesdata.herokuapp.com/api/cancer/withlocations"
    PARAMS = {"Accept: application/json"}

    if num > 0:
        url += "?limit="+str(num)

    r = requests.get(url=url, params=str(PARAMS))
    r = r.json()

    D = r["data"]

    # c = CancerIncidence.CancerIncidence()
    for i in range(len(D)):
        c = cancer_incidence.CancerIncidence()
        v = D[i]
        age = v["Age"]
        c.set_age_adjusted_rate(age["Age Adjusted Rate"])
        c.set_age_adjusted_ci_lower(age["Age Adjusted CI Lower"])
        c.set_age_adjusted_ci_upper(age["Age Adjusted CI Upper"])
        c.set_year(v["Year"])

        data = v["Data"]

        c.set_crude_rate(data["Crude Rate"])
        c.set_crude_rate_ci_lower(data["Crude CI Lower"])
        c.set_crude_rate_ci_upper(data["Crude CI Upper"])
        c.set_race(data["Race"])
        c.set_population(data["Population"])
        c.set_event_type(data["Event Type"])
        c.set_affected_area(v["Area"])

        loc = v["loc"]

        c.set_location_x(loc[0])
        c.set_location_y(loc[1])

        wrapper.append(c)
    return wrapper


def get_song(songTitle, artistName = None):
    """
    @brief Get data of a particular song (including lyrics) using the Genius API.

    Data from Genius API(https://docs.genius.com/), given the song title and artist name.
    Valid endpoints:  http://bridgesdata.herokuapp.com/api/songs/find/
    Valid queryParams: song title, artist name

    This function retrieves  and formats the data into a
    Song object. The song if not cached in the local DB is queried
    and added to the DB

    Args:
        songTitle: title of song
        artistName: name of artist

    @throws Exception if the request fails

    Returns:
         a Song object,
    """
    wrapper = []
    url = "http://bridgesdata.herokuapp.com/api/songs/find/"
    PARAMS = {"Accept: application/json"}

    if len(songTitle):
        url = url + urllib.parse.quote(songTitle)

    if artistName is not None:
        if len(artistName):
            url += "?artistName=" + urllib.parse.quote(artistName)

    r = requests.get(url = url, params = str(PARAMS))
    if r.status_code != 200:
        raise ConnectionError("HTTP Request Failed. Error Code: " + r.status_code)
    r = r.json()
    if "artist" in r:
        artist = r["artist"]
    else:
        artist = ""
    if "song" in r:
        songs = r["song"]
    else:
        songs = ""
    if "album" in r:
        album = r["album"]
    else:
        album = ""
    if "lyrics" in r:
        lyrics = r["lyrics"]
    else:
        lyrics = ""
    if "release_date" in r:
        release_date = r["release_date"]
    else:
        release_date = ""

    return song.Song(artist, songs, album, lyrics, release_date)


def get_song_data():
    """

    Get data of the songs (including lyrics) using the Genius API.

    Song data from https://docs.genius.com/.
    Valid endpoints:  https://bridgesdata.herokuapp.com/api/songs/

    This function retrieves  and formats the data into a list of
    Song objects. This version of the API retrieves all the cached
    songs in the local DB.

    @throws Exception if the request fails

    Returns:
         A list of Song objects,
    """
    all_songs = []
    url = "http://bridgesdata.herokuapp.com/api/songs/"
    params = {"Accept: application/json"}

    r = requests.get(url=url, params=str(params))
    r = r.json()

    D = r["data"]

    for i in range(len(D)):
        v = D[i]

        if "artist" in v:
            artist = v["artist"]
        else:
            artist = ""

        if "song" in v:
            song = v["song"]
        else:
            song = ""

        if "album" in v:
            album = v["album"]
        else:
            album = ""

        if "lyrics" in v:
            lyrics = v["lyrics"]
        else:
            lyrics = ""

        if "release_date" in v:
            release_date = v["release_date"]
        else:
            release_date = ""

        all_songs.append(song.Song(artist, song, album, lyrics, release_date))
    return all_songs


def get_color_grid_from_assignment(server: str, user: str, assignment: int, subassignment: int = 0) -> ColorGrid:
    """
    @brief Reconstruct a ColorGrid from an existing ColorGrid on the bridges server

    This method can be useful in early CS courses like CS1 or CS2  to 
    work with an existing data structure holding a dataset, like an image.

    Args:
        server:  server holding the assignment
        user:  user name
        assignment:  assignment number
        subassignment:  sub-assignment number

    Returns:
        ColorGrid structure
    """

    response = get_assignment(server, user, assignment, subassignment)

    try:
        from types import SimpleNamespace as Namespace
    except ImportError:
        # Python 2.x fallback
        from argparse import Namespace

    assignment_object = json.loads(response, object_hook=lambda d: Namespace(**d))
    try:
        if assignment_object.assignment_type != "ColorGrid":
            raise RuntimeError("Malformed ColorGrid JSON: not a ColorGrid")

        data_list = assignment_object.data
        if len(data_list) != 1:
            raise RuntimeError("Malformed JSON: data is malformed")
        data = data_list[0]
        try:
            encoding = data.encoding
        except AttributeError:
            # Handle case for ColorGrid generated before encoding field was added
            encoding = "RAW"

        if encoding != "RLE" and encoding != "RAW":
            raise RuntimeError("Malformed ColorGrid JSON: encoding not supported: " + encoding)

        if len(data.dimensions) != 2:
            raise RuntimeError("Malformed ColorGrid JSON: dimensions are malformed")
        dim_x = int(data.dimensions[0])
        dim_y = int(data.dimensions[1])

        try:
            node_string = str(data.nodes)
        except (TypeError, ValueError):
            raise RuntimeError("Malformed ColorGrid JSON: node is not a String")

        import base64
        decoded_bytes = bytearray(base64.b64decode(node_string))
        decoded = [int(x) for x in decoded_bytes]
    except AttributeError:
        raise RuntimeError("Malformed JSON: Unable to Parse. Does this assignment exist?")

    color_grid = ColorGrid(dim_x, dim_y)

    if encoding == "RAW":
        if len(decoded) != dim_x * dim_y * 4:
            raise RuntimeError("Malformed ColorGrid JSON: nodes is not the size we expect for RAW encoding")

        base = 0
        for x in range(0, dim_x):
            for y in range(0, dim_y):
                color = Color(decoded[base],
                              decoded[base + 1],
                              decoded[base + 2],
                              (decoded[base + 3]/255.0)
                              )
                color_grid.set(x, y, color)
                base = base + 4

    elif encoding == "RLE":
        if len(decoded) % 5:
            raise RuntimeError("Malformed ColorGrid JSON: RLE nodes are not a multiple of 5")
        current_in_decoded = 0
        current_in_cg = 0
        while current_in_decoded != len(decoded):
            repeat = decoded[current_in_decoded]

            color = Color(decoded[current_in_decoded + 1],
                          decoded[current_in_decoded + 2],
                          decoded[current_in_decoded + 3],
                          (decoded[current_in_decoded + 4] / 255.0)
                          )
            current_in_decoded = current_in_decoded + 5

            while repeat >= 0:
                pos_x = int(current_in_cg / dim_y)
                pos_y = int(current_in_cg % dim_y)
                if pos_x >= dim_x or pos_y >= dim_y:
                    raise RuntimeError("Malformed ColorGrid JSON: Too much data in nodes")

                color_grid.set(pos_x, pos_y, color)
                current_in_cg = current_in_cg + 1
                repeat = repeat - 1

    return color_grid


def get_assignment(server: str, user: str, assignment: int, subassignment: int = 0) -> str:
    """
    @brief This method retrieves a stored assignment from the server.

    Args:
        server:  server holding the assignment
        user: the name of the user who uploaded the assignment
        assignment:  assignment number
        subassignment:  sub-assignment number

    Returns:
        data structure representation of the assignment as a JSON string
    """
    subassignment_fixed = "0{}".format(subassignment) if subassignment < 10 else subassignment
    url = "{}/assignmentJSON/{}.{}/{}".format(server, assignment, subassignment_fixed, user)
    params = "Accept: application/json"

    request = requests.get(url, params)
    if request.ok:
        return request.content
    else:
        raise request.raise_for_status()


def osm_server_request(url):
    """
    @brief Fulfills a server request for OpenStreetMap

    For internal use only

    Args:
        url: url for server to process

    """
    request = requests.get(url)
    if not request.ok:
        if request.status_code == 404:
            request = requests.get(url)
            if not request.ok:
                raise request.raise_for_status()
            raise RuntimeError("Location: {} is not supported,\n valid names: {}".format(location, valid_names))
        raise request.raise_for_status()

    server_data = request.content

    return server_data

def _get_osm_baseurl():
    return "http://bridges-data-server-osm.bridgesuncc.org/"

def get_osm_data(*args) -> OsmData:
    """
    @brief This method retrieves an OpenStreet Map dataset, given a location
    by name (string).

    Args:
        args: location name

    Returns:
        OSM data
    """
    import os

    debug = True
    
    if (len(args) == 2):
        location = args[0]
        level = args[1]
        url = _get_osm_baseurl()+"/loc?location=" + urllib.parse.quote(location) + "&level=" + urllib.parse.quote(level)
        hash_url = _get_osm_baseurl()+"/hash?location=" + urllib.parse.quote(location) + "&level=" + urllib.parse.quote(level)
    elif (len(args) == 5):
        minLat = str(args[0])
        minLon = str(args[1])
        maxLat = str(args[2])
        maxLon = str(args[3])
        level = args[4]
        url = _get_osm_baseurl()+"/coords?minLon=" + minLon + "&minLat=" + minLat + "&maxLon=" + maxLon + "&maxLat=" + maxLat + "&level=" + level
        hash_url = _get_osm_baseurl()+"/hash?minLon=" + minLon + "&minLat=" + minLat + "&maxLon=" + maxLon + "&maxLat=" + maxLat + "&level=" + level
    else:
        raise RuntimeError("Invalid Map Request Inputs")

    lru = lru_cache.lru_cache(30)

    if debug:
        print ("url: "+url)
        print ("hash_url: "+hash_url)

    try:
        from types import SimpleNamespace as Namespace
    except ImportError:
        from argparse import Namespace


    data = None
    not_skip = True
    hash = osm_server_request(hash_url).decode('utf-8')
    if (hash != "false" and lru.inCache(hash)):
        not_skip = False
        data = lru.get(hash)

    if not_skip:
        content = osm_server_request(url)
        try:
            data = json.loads(content.decode('utf-8'), object_hook=lambda d: Namespace(**d))
        except:
            print("Error: Corrupted JSON download...\nAttempting redownload...")
            content = osm_server_request(url)
            try:
                data = json.loads(content.decode('utf-8'), object_hook=lambda d: Namespace(**d))
            except Exception as e:
                print(f"Error: Redownload attempt failed\n{e}")
                sys.exit(0)

        hash = osm_server_request(hash_url).decode('utf-8')
        lru.put(hash, data)


    try:
        if data.nodes is None or data.edges is None or data.meta is None:
            raise RuntimeError("Malformed OSM JSON")

        vertex_map = {}
        vertices = []
        for i, node in enumerate(data.nodes):
            vertex_map[node[0]] = i
            vertices.append(OsmVertex(node[1], node[2]))

        edges = []
        for edge in data.edges:
            id_from = edge[0]
            id_to = edge[1]
            dist = edge[2]
            edges.append(OsmEdge(vertex_map[id_from], vertex_map[id_to], dist))

        ret_osm = OsmData()
        ret_osm.edges = edges
        ret_osm.vertices = vertices
        ret_osm.name = data.meta.name
        return ret_osm

    except AttributeError:
        raise RuntimeError("Malformed JSON: Unable to parse")

def server_request(url):
    """
    @brief Fulfills a server request
    
    For internal use only
    """
    request = requests.get(url)
    if not request.ok:
        if request.status_code == 404:
            request = requests.get(url)
            if not request.ok:
                raise request.raise_for_status()
            raise RuntimeError("Issue with server request")
        raise request.raise_for_status()

    server_data = request.content

    return server_data

def get_elevation_data(*args):
    """
    @brief This method retrieves an Elevation  Map dataset, given a location
        by name (string).
    Args:
        args(0): a bounding box, aka an array [minLat, minLon, maxLat, maxLon]
        args(1): spatial resolution, aka the distance between two samples (in degrees)

    Returns:
        Elevation data for the bounding box and resolution requested 
        (approximately) [type: ElevationData]
    """
    base_url = "http://bridges-data-server-elevation.bridgesuncc.org/elevation"
    hash_url = "http://bridges-data-server-elevation.bridgesuncc.org/hash"

    coords = args[0]
    minLat = str(coords[0])
    minLon = str(coords[1])
    maxLat = str(coords[2])
    maxLon = str(coords[3])

    res = .0166
        
    if len(args) == 2:
        res = args[1]
        
    url = base_url + f"?minLat={minLat}&minLon={minLon}&maxLat={maxLat}&maxLon={maxLon}&resX={res}&resY={res}"
    hash_url = hash_url + f"?minLat={minLat}&minLon={minLon}&maxLat={maxLat}&maxLon={maxLon}&resX={res}&resY={res}"
    #loads cache
    lru = lru_cache.lru_cache(30)

    data = None
    not_skip = True
    hash = False
    hash = server_request(hash_url).decode('utf-8')
    if (hash != "false" and lru.inCache(hash)):
        not_skip = False
        data = lru.get(hash)

    if not_skip:
        data = server_request(url).decode("utf-8")

    hash = server_request(hash_url).decode('utf-8')
    lru.put(hash, data)

    
    #parse and build object
    ret_ele = ElevationData()
    
    file_array = data.splitlines()
    ret_ele.cols = int(file_array[0].split(" ")[-1])
    ret_ele.rows = int(file_array[1].split(" ")[-1])
    ret_ele._xll = float(file_array[2].split(" ")[-1])
    ret_ele._yll = float(file_array[3].split(" ")[-1])
    ret_ele.cellsize = float(file_array[4].split(" ")[-1])

    maxVal = -9999999999
    x = 5
    while (x < len(file_array)):
        arr = file_array[x].replace("\n", "").split(" ")
        arr.pop(0)
        parsedline = []
        for y in arr:
            parsedline.append(int(y))
            if(int(y) > maxVal):
                maxVal = int(y)
        ret_ele.data.append(parsedline)
        x += 1
    ret_ele.maxVal = maxVal

    return ret_ele

def _get_wiki_actor_movie_direct(year_begin, year_end, array_out):
    """
    @brief get actor movie data from wiki data
    Args:
        year_begin: beginning year of data
        year_end: ending year of data.
        array_out: returned data in an array
    """
    # allocate cache - this makes no sense
    lru = lru_cache.lru_cache(30)

    # check cache code name and check cache
    code_name = "wikidata-actormovie-" + str(year_begin) + "-" + str(year_end)
    print(code_name)
    from_cache = False
    if lru.inCache(code_name):  
        results = lru.get(code_name)
        from_cache = True
    else:
        sparql = SPARQLWrapper("https://query.wikidata.org/sparql")
        sparql.setQuery("""
            SELECT ?movie ?movieLabel ?actor ?actorLabel WHERE \
            {\
                ?movie wdt:P31 wd:Q11424.\
                ?movie wdt:P161 ?actor.\
                ?movie wdt:P364 wd:Q1860.\
                ?movie wdt:P577 ?date.\
                FILTER(YEAR(?date) >= """ + str(year_begin) + """ && YEAR(?date) <= """ + str(year_end) + """).\
                SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\". } \
            }
        """)
        sparql.addCustomHttpHeader("User-Agent", 'bridges-python')
        sparql.setReturnFormat(JSON)
        results = sparql.query().convert()
        lru.put(code_name, results)

    for result in results["results"]["bindings"]:
        mak = movie_actor_wiki_data.MovieActorWikiData()
        actor_uri = str(result['actor']['value'])
        movie_uri = str(result['movie']['value'])
        actor_uri = actor_uri.replace("http://www.wikidata.org/entity/","",1)
        movie_uri = movie_uri.replace("http://www.wikidata.org/entity/","",1)
        mak.actor_uri = actor_uri
        mak.movie_uri = movie_uri
        mak.movie_name = str(result['movieLabel']['value'])
        mak.actor_name = str(result['actorLabel']['value'])
        array_out.append(mak)
        # print(result['movie']['value'])

def get_wiki_data_actor_movie(year_begin, year_end):
    """
    @brief This method retrieves an actor-movie data from Wikidata

    Uses a sparql query

    Args:
        year_begin:  beginning year of data request
        year_end:    ending year of data request

    Returns:
        A list of the actor-movie data of type MovieActorWikiData
    """
    ret = []
    for y in range(year_begin, year_end+1):
        _get_wiki_actor_movie_direct(y, y, ret)
    return ret

def get_amenity_data(*args):
    """
    @brief This method retrieves amenity data from Open Street Map datasets.
    
    Args:
        [0]: minimum latitude
        [1]: minimum longitude
        [2]: maximum latitude
        [3]: maximum longitude
        [4]: amenity type
      or
        [0]: location string
        [1]: amenity type

    Valid types are "food", "school", "firestation", "airport", "heli"

    Returns:
        A list of amenities
    """
    
    if(len(args)) == 5:
        url = f"{_get_osm_baseurl()}/amenity?minLat={args[0]}&minLon={args[1]}&maxLat={args[2]}&maxLon={args[3]}&amenity={args[4]}"
        hash_url = f"{_get_osm_baseurl()}/hash?minLat={args[0]}&minLon={args[1]}&maxLat={args[2]}&maxLon={args[3]}&amenity={urllib.parse.quote(args[4])}"
    elif(len(args) == 2):
        url = f"{_get_osm_baseurl()}/amenity?location={urllib.parse.quote(args[0])}&amenity={urllib.parse.quote(args[1])}"
        hash_url = f"{_get_osm_baseurl()}/hash?location={args[0]}&amenity={args[1]}"
    else:
        raise RuntimeError("Invalid Number of Map Request Inputs")

    lru = lru_cache.lru_cache(30)
    data = None
    not_skip = True

    hash = osm_server_request(hash_url).decode('utf-8')
    if (hash != "false" and lru.inCache(hash)):
        not_skip = False
        data = lru.get(hash)
    
    if (not_skip):
        content =  osm_server_request(url)
        try:
            data = json.loads(content.decode('utf-8'))
        except:
            print("Error: Corrupted JSON download...\nAttempting redownload...")
            content = osm_server_request(url)
            try:
                data = json.loads(content.decode('utf-8'))
            except Exception as e:
                print(f"Error: Redownload attempt failed\n{e}")
                raise RuntimeError(e)
                sys.exit(0)

        hash = osm_server_request(hash_url).decode('utf-8')
        lru.put(hash, data)


    ret_data = amenities()


    for i, node in enumerate(data['nodes']):
        temp = amenityData()
        for x, vals in enumerate(node):
            if (x == 0):
                temp.id = vals
            elif (x == 1):
                temp.lat = vals
            elif (x == 2):
                temp.lon = vals
            elif (x == 3):
                temp.name = vals
            else:
                temp.addOther = vals
        ret_data.data = temp
    
    temp_meta = meta(data['meta']['minlat'], data['meta']['minlon'], data['meta']['maxlat'], data['meta']['maxlon'], data['meta']['count'])
    ret_data.meta = temp_meta

    return ret_data

def search_gutenberg(*args):
    url = f"http://192.168.2.14:5000"
    url = url + f"/search?search={args[0]}&type={args[1]}"

    content = server_request(url)
    data = json.loads(content.decode('utf-8'))

    meta_obj_list = []
    for node in data['book_list']:
        meta = gutenberg_meta.GutenbergMeta

        meta.id = node["id"]
        meta.title = node['title']
        meta.lang = node["lang"]
        meta.date = node["date_added"]
        meta.authors = node["authors"]
        meta.genres = node["genres"]
        meta.loc = node["loc_class"]

        meta_obj_list.append(meta)

    return meta_obj_list

def meta_gutenberg(id):
    url = "http://192.168.2.14:5000"
    url = url + "/meta?id=" + str(id)

    content = server_request(url)
    data = json.loads(content.decode('utf-8'))

    meta = gutenberg_meta.GutenbergMeta
    for node in data['book_list']:
        
        
        meta.id = node["id"]
        meta.title = node['title']
        meta.lang = node["lang"]
        meta.date = node["date_added"]
        meta.authors = node["authors"]
        meta.genres = node["genres"]
        meta.loc = node["loc_class"]
        
        
        
    return meta

def text_gutenberg(id, strip = False):
    url = "http://192.168.2.14:5000"
    url = url + "/book?id=" + str(id)

    content = server_request(url)
    data = content.decode('utf-8')

    # TODO: Cache book text locally (use ID as unique identifier)

    return data



class DataSource:
    pass
