import sys
import os
import datetime as dt
import unicodedata
import numpy as np
import logging as lg
from . import settings


def citation():
    """
    Print the OSMnx package's citation information.

    Boeing, G. 2017. OSMnx: New Methods for Acquiring, Constructing, Analyzing,
    and Visualizing Complex Street Networks. Computers, Environment and Urban
    Systems, 65(126-139). https://doi.org/10.1016/j.compenvurbsys.2017.05.004
    """

    cite = ("To cite OSMnx, use:\n\n"
            "Boeing, G. 2017. OSMnx: New Methods for Acquiring, Constructing, Analyzing, "
            "and Visualizing Complex Street Networks. Computers, Environment and Urban "
            "Systems, 65(126-139). https://doi.org/10.1016/j.compenvurbsys.2017.05.004"
            "\n\n"
            "BibTeX entry for LaTeX users:\n\n"

            "@article{boeing_osmnx_2017,\n"
            "    title = {{OSMnx}: {New} {Methods} for {Acquiring}, {Constructing}, {Analyzing}, and {Visualizing} {Complex} {Street} {Networks}},\n"
            "    volume = {65},\n"
            "    doi = {10.1016/j.compenvurbsys.2017.05.004},\n"
            "    number = {126-139},\n"
            "    journal = {Computers, Environment and Urban Systems},\n"
            "    author = {Boeing, Geoff},\n"
            "    year = {2017}\n"
            "}")

    print(cite)


def make_str(value):
    """
    Convert a passed-in value to unicode if Python 2, or string if Python 3.

    Parameters
    ----------
    value : any
        the value to convert to unicode/string

    Returns
    -------
    unicode or string
    """
    if (sys.version_info > (3, 0)):
        # python 3.x has no unicode type, so if error, use str type
        return str(value)
    else:
        # for python 2.x compatibility, use unicode
        return unicode(value)


def config(data_folder=settings.data_folder,
           logs_folder=settings.logs_folder,
           imgs_folder=settings.imgs_folder,
           cache_folder=settings.cache_folder,
           use_cache=settings.use_cache,
           log_file=settings.log_file,
           log_console=settings.log_console,
           log_level=settings.log_level,
           log_name=settings.log_name,
           log_filename=settings.log_filename,
           useful_tags_node=settings.useful_tags_node,
           useful_tags_path=settings.useful_tags_path,
           osm_xml_node_attrs=settings.osm_xml_node_attrs,
           osm_xml_node_tags=settings.osm_xml_node_tags,
           osm_xml_way_attrs=settings.osm_xml_way_attrs,
           osm_xml_way_tags=settings.osm_xml_way_tags,
           default_access=settings.default_access,
           default_crs=settings.default_crs,
           default_user_agent=settings.default_user_agent,
           default_referer=settings.default_referer,
           default_accept_language=settings.default_accept_language,
           nominatim_endpoint=settings.nominatim_endpoint,
           nominatim_key=settings.nominatim_key,
           overpass_endpoint=settings.overpass_endpoint,
           all_oneway=settings.all_oneway):
    """
    Configure osmnx by setting the default global vars to desired values.

    Parameters
    ---------
    data_folder : string
        where to save and load data files
    logs_folder : string
        where to write the log files
    imgs_folder : string
        where to save figures
    cache_folder : string
        where to save the http response cache
    use_cache : bool
        if True, use a local cache to save/retrieve http responses instead of
        calling API repetitively for the same request URL
    log_file : bool
        if true, save log output to a log file in logs_folder
    log_console : bool
        if true, print log output to the console
    log_level : int
        one of the logger.level constants
    log_name : string
        name of the logger
    useful_tags_node : list
        a list of useful OSM tags to attempt to save from node elements
    useful_tags_path : list
        a list of useful OSM tags to attempt to save from path elements
    default_access : string
        default filter for OSM "access" key
    default_crs : string
        default CRS to set when creating graphs
    default_user_agent : string
        HTTP header user-agent
    default_referer : string
        HTTP header referer
    default_accept_language : string
        HTTP header accept-language
    nominatim_endpoint : string
        which API endpoint to use for nominatim queries
    nominatim_key : string
        your API key, if you are using an endpoint that requires one
    overpass_endpoint : string
        which API endpoint to use for overpass queries
    all_oneway : boolean
        if True, forces all paths to be loaded as oneway ways, preserving
        the original order of nodes stored in the OSM way XML.

    Returns
    -------
    None
    """

    # set each global variable to the passed-in parameter value
    settings.use_cache = use_cache
    settings.cache_folder = cache_folder
    settings.data_folder = data_folder
    settings.imgs_folder = imgs_folder
    settings.logs_folder = logs_folder
    settings.log_console = log_console
    settings.log_file = log_file
    settings.log_level = log_level
    settings.log_name = log_name
    settings.log_filename = log_filename
    settings.useful_tags_node = useful_tags_node
    settings.useful_tags_path = useful_tags_path
    settings.useful_tags_node = list(set(useful_tags_node + osm_xml_node_attrs + osm_xml_node_tags))
    settings.useful_tags_path = list(set(useful_tags_path + osm_xml_way_attrs + osm_xml_way_tags))
    settings.osm_xml_node_attrs = osm_xml_node_attrs
    settings.osm_xml_node_tags = osm_xml_node_tags
    settings.osm_xml_way_attrs = osm_xml_way_attrs
    settings.osm_xml_way_tags = osm_xml_way_tags
    settings.default_access = default_access
    settings.default_crs = default_crs
    settings.default_user_agent = default_user_agent
    settings.default_referer = default_referer
    settings.default_accept_language = default_accept_language
    settings.nominatim_endpoint = nominatim_endpoint
    settings.nominatim_key = nominatim_key
    settings.overpass_endpoint = overpass_endpoint
    settings.all_oneway = all_oneway

    # if logging is turned on, log that we are configured
    if settings.log_file or settings.log_console:
        log('Configured osmnx')


def great_circle_vec(lat1, lng1, lat2, lng2, earth_radius=6371009):
    """
    Vectorized function to calculate the great-circle distance between two
    points or between vectors of points, using haversine.

    Parameters
    ----------
    lat1 : float or array of float
    lng1 : float or array of float
    lat2 : float or array of float
    lng2 : float or array of float
    earth_radius : numeric
        radius of earth in units in which distance will be returned (default is
        meters)

    Returns
    -------
    distance : float or vector of floats
        distance or vector of distances from (lat1, lng1) to (lat2, lng2) in
        units of earth_radius
    """

    phi1 = np.deg2rad(lat1)
    phi2 = np.deg2rad(lat2)
    d_phi = phi2 - phi1

    theta1 = np.deg2rad(lng1)
    theta2 = np.deg2rad(lng2)
    d_theta = theta2 - theta1

    h = np.sin(d_phi / 2) ** 2 + np.cos(phi1) * np.cos(phi2) * np.sin(d_theta / 2) ** 2
    h = np.minimum(1.0, h)  # protect against floating point errors

    arc = 2 * np.arcsin(np.sqrt(h))

    # return distance in units of earth_radius
    distance = arc * earth_radius
    return distance


def euclidean_dist_vec(y1, x1, y2, x2):
    """
    Vectorized function to calculate the euclidean distance between two points
    or between vectors of points.

    Parameters
    ----------
    y1 : float or array of float
    x1 : float or array of float
    y2 : float or array of float
    x2 : float or array of float

    Returns
    -------
    distance : float or array of float
        distance or vector of distances from (x1, y1) to (x2, y2) in graph units
    """

    # euclid's formula
    distance = ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
    return distance


def log(message, level=None, name=None, filename=None):
    """
    Write a message to the log file and/or print to the the console.

    Parameters
    ----------
    message : string
        the content of the message to log
    level : int
        one of the logger.level constants
    name : string
        name of the logger
    filename : string
        name of the log file

    Returns
    -------
    None
    """

    if level is None:
        level = settings.log_level
    if name is None:
        name = settings.log_name
    if filename is None:
        filename = settings.log_filename

    # if logging to file is turned on
    if settings.log_file:
        # get the current logger (or create a new one, if none), then log
        # message at requested level
        logger = get_logger(level=level, name=name, filename=filename)
        if level == lg.DEBUG:
            logger.debug(message)
        elif level == lg.INFO:
            logger.info(message)
        elif level == lg.WARNING:
            logger.warning(message)
        elif level == lg.ERROR:
            logger.error(message)

    # if logging to console is turned on, convert message to ascii and print to
    # the console
    if settings.log_console:
        # capture current stdout, then switch it to the console, print the
        # message, then switch back to what had been the stdout. this prevents
        # logging to notebook - instead, it goes to console
        standard_out = sys.stdout
        sys.stdout = sys.__stdout__

        # convert message to ascii for console display so it doesn't break
        # windows terminals
        message = unicodedata.normalize('NFKD', make_str(message)).encode('ascii', errors='replace').decode()
        print(message)
        sys.stdout = standard_out


def get_logger(level=None, name=None, filename=None):
    """
    Create a logger or return the current one if already instantiated.

    Parameters
    ----------
    level : int
        one of the logger.level constants
    name : string
        name of the logger
    filename : string
        name of the log file

    Returns
    -------
    logger.logger
    """

    if level is None:
        level = settings.log_level
    if name is None:
        name = settings.log_name
    if filename is None:
        filename = settings.log_filename

    logger = lg.getLogger(name)

    # if a logger with this name is not already set up
    if not getattr(logger, 'handler_set', None):

        # get today's date and construct a log filename
        todays_date = dt.datetime.today().strftime('%Y_%m_%d')
        log_filename = os.path.join(settings.logs_folder, '{}_{}.log'.format(filename, todays_date))

        # if the logs folder does not already exist, create it
        if not os.path.exists(settings.logs_folder):
            os.makedirs(settings.logs_folder)

        # create file handler and log formatter and set them up
        handler = lg.FileHandler(log_filename, encoding='utf-8')
        formatter = lg.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s')
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        logger.setLevel(level)
        logger.handler_set = True

    return logger


def get_unique_nodes_ordered_from_way(way_edges_df):
    """
    Function to recover the original order of nodes from a dataframe
    of edges associated with a single OSM way.

    Parameters
    ----------
    way_edges_df : pandas.DataFrame()
        Dataframe containing columns 'u' and 'v' corresponding to
        origin/desitination nodes.

    Returns
    -------
    unique_ordered_nodes : list
        An ordered list of unique node IDs

    NOTE: If the edges do not all connect (e.g. [(1, 2), (2,3),
    (10, 11), (11, 12), (12, 13)]), then this method will return
    only those nodes associated with the FIRST chunk of connected
    edges, even if subsequent connected chunks are contain more
    total nodes. I don't believe that we would ever encounter this
    kind of disconnected structure of nodes within a given way,
    but as best I could tell it is not explicitly forbidden in the
    OSM XML design schema. As such, I had to safeguard against it
    to ensure this method wouldn't get stuck in the while loop if
    encountered a disconnected structure. I'm using a print
    statement right now to tell the user whether or not any nodes
    have been dropped and how many.
    """
    all_nodes = list(way_edges_df['u'].values) + \
        list(way_edges_df['v'].values)
    num_unique_nodes = len(np.unique(all_nodes))
    node_pairs = list(way_edges_df[['u', 'v']].values)
    unique_ordered_nodes = []
    recycled = []

    while len(node_pairs) > 0:

        pair = node_pairs.pop(0)
        start = pair[0]
        end = pair[1]
        the_rest = [element for p in node_pairs for element in p]

        # first pair
        if len(unique_ordered_nodes) == 0:

            # if there are subsequent pairs to match on
            if start in the_rest or end in the_rest:
                unique_ordered_nodes = list(pair)
            continue

        # if both nodes are already in the list, we don't need them
        if (start in unique_ordered_nodes) and (end in unique_ordered_nodes):
            continue

        # if start node is in the list, add the end node to the right of it
        if start in unique_ordered_nodes:
            start_idx = unique_ordered_nodes.index(start)
            end_idx = start_idx + 1
            unique_ordered_nodes[end_idx:end_idx] = [end]

        # if end node is in the list, add the start node to the left of it
        elif end in unique_ordered_nodes:
            end_idx = unique_ordered_nodes.index(end)
            start_idx = end_idx
            unique_ordered_nodes[start_idx:start_idx] = [start]

        else:
            # if we've already processed this pair and there is still no way
            # to match it in the list, then we're done
            if list(pair) in recycled:
                break

            # if there's no match in the list but there's a match in the
            # remaining pairs to be processed, recycle it to process again
            elif start in the_rest or end in the_rest:
                node_pairs.append(pair)
                recycled.append(list(pair))

    if len(unique_ordered_nodes) < num_unique_nodes:
        print('Recovered order for {0} of {1} nodes'.format(
            len(unique_ordered_nodes), num_unique_nodes))

    return unique_ordered_nodes
