# coding=utf-8
"""Helper utilities module to compute various statistics for the current AOI.
:copyright: (c) 2013 by Tim Sutton
:license: GPLv3, see LICENSE for more details.
"""
import os
import sys
import getpass
from tempfile import mkstemp
import xml
import time
from datetime import date, timedelta
import zipfile

from reporter import config
from reporter.osm_node_parser import OsmNodeParser
from reporter.osm_way_parser import OsmParser
from reporter.queries import RESOURCES_MAP
from reporter import LOGGER


def overpass_resource_base_path(feature_type):
    """Get the overpass resource base path according to the feature we extract.

    :param feature_type: The type of feature :
        buildings, building-points, roads, potential-idp, boundary-[1,11]
    :type feature_type: str

    :return The resource folder.
    :rtype str
    """
    return os.path.abspath(
        os.path.join(
            os.path.dirname(__file__),
            'resources',
            'overpass',
            RESOURCES_MAP[feature_type],
            RESOURCES_MAP[feature_type]))


def shapefile_resource_base_path(feature_type):
    """Get the shapefile resource base path according to the feature we extract.

    :param feature_type: The type of feature :
        buildings, building-points, roads, potential-idp, boundary-[1,11]
    :type feature_type: str

    :return The resource folder.
    :rtype str
    """
    return os.path.abspath(
        os.path.join(
            os.path.dirname(__file__),
            'resources',
            'shapefile',
            RESOURCES_MAP[feature_type],
            RESOURCES_MAP[feature_type]))


def generic_shapefile_base_path():
    """Get the generic shapefile resource base path.

    :return The generic resource folder.
    :rtype str
    """
    return os.path.abspath(
        os.path.join(
            os.path.dirname(__file__),
            'resources',
            'shapefile',
            'generic',
            'generic'))


def get_totals(sorted_user_list):
    """Given a sorted user list, get the totals for ways and nodes.

    :param sorted_user_list: User dicts sorted by number of ways.
    :type sorted_user_list: list

    :returns: Two-tuple (int, int) containing way count, node count.
    :rtype: (int, int)
    """
    way_count = 0
    node_count = 0
    for user in sorted_user_list:
        way_count += user['ways']
        node_count += user['nodes']
    return node_count, way_count


def split_bbox(bbox):
    """Split a bounding box into its parts.

    :param bbox: String describing a bbox e.g. '106.78674459457397,
        -6.141301491467023,106.80691480636597,-6.133834354201348'
    :type bbox: str

    :returns: A dict with keys: 'southwest_lng, southwest_lat, northeast_lng,
            northeast_lat'
    :rtype: dict

    """
    values = bbox.split(',')
    if not len(values) == 4:
        raise ValueError('Invalid bbox')
    # pylint: disable=W0141
    # next line should probably use list comprehension rather
    # http://pylint-messages.wikidot.com/messages:w0141
    values = map(float, values)
    # pylint: enable=W0141
    names = ['SW_lng', 'SW_lat', 'NE_lng', 'NE_lat']
    coordinates = dict(zip(names, values))
    return coordinates


def split_polygon(polygon):
    """Split polygon array to string.

    :param polygon: list of array describing polygon area e.g.
    '[[28.01513671875,-25.77516058680343],[28.855590820312504,-25.567220388070023],
    [29.168701171875004,-26.34265280938059]]
    :type polygon: list

    :returns: A string of polygon e.g. 50.7 7.1 50.7 7.12 50.71 7.11
    :rtype: str
    """

    if len(polygon) < 3:
        raise ValueError(
                'At least 3 lat/lon float value pairs must be provided')

    polygon_string = ''

    for poly in polygon:
        polygon_string += ' '.join(map(str, poly))
        polygon_string += ' '

    return polygon_string.strip()


def osm_object_contributions(
        osm_file,
        tag_name,
        date_start=None,
        date_end=None):
    """Compile a summary of user contributions for the selected osm data type.

    :param osm_file: A file object reading from a .osm file.
    :type osm_file: file, FileIO

    :param tag_name: The tag name we want to filter on.
    :type tag_name: str

    :param date_start: The start date we want to filter
    :type date_start: float

    :param date_end: The end date we want to filter
    :type date_end: float

    :returns: A list of dicts where items in the list are sorted from highest
        contributor (based on number of ways) down to lowest. Each element
        in the list is a dict in the form: {
        'user': <user>,
        'ways': <way count>,
        'nodes': <node count>,
        'timeline': <timelinedict>,
        'best': <most ways in a single day>,
        'worst': <least ways in single day>,
        'average': <average ways across active days>,
        'crew': <bool> }
        where crew is used to designate users who are part of an active
        data gathering campaign.
        The timeline dict will contain a collection of dates and
        the total number of ways created on that date e.g.
        {
            u'2010-12-09': 10,
            u'2012-07-10': 14
        }
    :rtype: list
    """
    parser = OsmParser(
            start_date=date_start,
            end_date=date_end)
    try:
        xml.sax.parse(osm_file, parser)
    except xml.sax.SAXParseException:
        LOGGER.exception('Failed to parse OSM xml.')
        raise

    way_count_dict = parser.wayCountDict
    node_count_dict = parser.nodeCountDict
    timelines = parser.userDayCountDict

    # Convert to a list of dicts so we can sort it.
    crew_list = config.CREW
    user_list = []

    for key, value in way_count_dict.items():
        start_date, end_date = date_range(timelines[key])
        start_date = time.strftime('%d-%m-%Y', start_date.timetuple())
        end_date = time.strftime('%d-%m-%Y', end_date.timetuple())
        user_timeline = timelines[key]
        node_count = 0
        if key in node_count_dict:
            node_count = node_count_dict[key]
        record = {
            'name': key,
            'ways': value,
            'nodes': node_count,
            'timeline': interpolated_timeline(user_timeline),
            'start': start_date,
            'end': end_date,
            'activeDays': len(user_timeline),
            'best': best_active_day(user_timeline),
            'worst': worst_active_day(user_timeline),
            'average': average_for_active_days(user_timeline)
        }
        user_list.append(record)

    for key, value in node_count_dict.items():
        start_date, end_date = date_range(timelines[key])
        start_date = time.strftime('%d-%m-%Y', start_date.timetuple())
        end_date = time.strftime('%d-%m-%Y', end_date.timetuple())
        user_timeline = timelines[key]
        record = {
            'name': key,
            'ways': 0,
            'nodes': value,
            'timeline': interpolated_timeline(user_timeline),
            'start': start_date,
            'end': end_date,
            'activeDays': len(user_timeline),
            'best': best_active_day(user_timeline),
            'worst': worst_active_day(user_timeline),
            'average': average_for_active_days(user_timeline)
        }
        user_list.append(record)

    # Sort it
    sorted_user_list = sorted(
        user_list, key=lambda d: (
            -d['ways'],
            d['nodes'],
            d['name'],
            d['timeline'],
            d['start'],
            d['end'],
            d['activeDays'],
            d['best'],
            d['worst'],
            d['average']))
    return sorted_user_list


def date_range(timeline):
    """Given a timeline, determine the start and end dates.

    The timeline may be sparse (containing fewer entries than all the dates
    between the min and max dates) and since it is a dict,
    the dates may be in any order.

    :param timeline: A dictionary of non-sequential dates (in YYYY-MM-DD) as
    keys and values (representing ways collected on that day).
    :type timeline: dict

    :returns: A tuple containing two dates:
        * start_date - a date object representing the earliest date in the
            time line.
        * end_date - a date object representing the newest date in the time
            line.
    :rtype: (date, date)

    """
    start_date = None
    end_date = None
    for next_date in timeline.keys():
        year, month, day = next_date.split('-')
        message = 'Date: %s' % next_date
        LOGGER.info(message)
        timeline_date = date(int(year), int(month), int(day))
        if start_date is None:
            start_date = timeline_date
        if end_date is None:
            end_date = timeline_date
        if timeline_date < start_date:
            start_date = timeline_date
        if timeline_date > end_date:
            end_date = timeline_date
    return start_date, end_date


def average_for_active_days(timeline):
    """Compute the average activity per active day in a sparse timeline.

    :param timeline: A dictionary of non-sequential dates (in YYYY-MM-DD) as
        keys and values (representing ways collected on that day).
    :type timeline: dict

    :returns: Number of entities captured per day rounded to the nearest int.
    :rtype: int
    """
    count = 0
    total = 0
    for value in list(timeline.values()):
        if value > 0:
            count += 1
            total += value
    # Python 3 seems to automagically turn integer maths into float if needed
    average = int(total / count)
    return average


def best_active_day(timeline):
    """Compute the best activity for a single active day in a sparse timeline.

    :param timeline: A dictionary of non-sequential dates (in YYYY-MM-DD) as
        keys and values (representing ways collected on that day).
    :type timeline: dict

    :returns: Number of entities captured for the user's best day.
    :rtype: int
    """
    best = 0
    for value in list(timeline.values()):
        if value > best:
            best = value
    return best


def worst_active_day(timeline):
    """Compute the worst activity for a single active day in a sparse timeline.

    :param timeline: A dictionary of non-sequential dates (in YYYY-MM-DD) as
        keys and values (representing ways collected on that day).
    :type timeline: dict

    :returns: Number of entities captured for the user's worst day.
    :rtype: int
    """
    if len(timeline) < 1:
        return 0
    worst = list(timeline.values())[0]
    for value in list(timeline.values()):
        if value == 0:  # should never be but just in case
            continue
        if value < worst:
            worst = value
    return worst


def interpolated_timeline(timeline):
    """Interpolate a timeline given a sparse timeline.

    A sparse timelines is a sequence of dates containing no days of zero
    activity. An interpolated timeline is a sequence of dates where there is
    an entry per day in the date range regardless of whether there was any
    activity or not.

    :param timeline: A dictionary of non-sequential dates (in YYYY-MM-DD) as
        keys and values (representing ways collected on that day).
    :type timeline: dict

    :returns:  An interpolated list where each date in the original input
        date is present, and all days where no total was provided are added
        to include that day.
    :rtype: list

    Given an input looking like this::

        {
            {u'2012-09-24': 1},
            {u'2012-09-21': 10},
            {u'2012-09-25': 5},
        }

    The returned list will be in the form::

        [
            [Date(2012,09,21), 10],
            [Date(2012,09,22), 0],
            [Date(2012,09,23), 0],
            [Date(2012,09,24), 1],
            [Date(2012,09,25), 5],
        ]
    """
    # Work out the earliest and latest day
    start_date, end_date = date_range(timeline)
    # Loop through them, adding an entry for each day
    time_line = '['
    for current_date in date_range_iterator(start_date, end_date):
        date_string = time.strftime('%Y-%m-%d', current_date.timetuple())
        if date_string in timeline:
            value = timeline[date_string]
        else:
            value = 0
        if value == 0:
            continue
        if time_line != '[':
            time_line += ','
        time_line += '["%s",%i]' % (date_string, value)
    time_line += ']'
    return time_line


def date_range_iterator(start_date, end_date):
    """Given two dates return a collection of dates between start and end.

    :param start_date: Date representing the start date.
    :type start_date: date

    :param end_date: Date representing the end date.
    :type end_date: date

    :returns: Iterable collection yielding dates.
    :rtype: iterable
    """
    for n in range(int((end_date - start_date).days) + 1):
        yield start_date + timedelta(n)


def osm_nodes_by_user(file_handle, username):
    """Obtain the nodes collected by a single user from an OSM file.

    :param file_handle: File handle to an open OSM XML document.
    :type file_handle: file

    :param username: Name of the user for whom nodes should be collected.
    :type username: str

    :returns: A list of nodes for the given user.
    :rtype: list
    """
    parser = OsmNodeParser(username)
    xml.sax.parse(file_handle, parser)
    return parser.nodes


def temp_dir(sub_dir='work'):
    """Obtain the temporary working directory for the operating system.

    An osm-reporter subdirectory will automatically be created under this.