# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
from eight import *
from future.utils import python_2_unicode_compatible

from .maps import Map, to_shape
from .projection import project, wgs84, MOLLWEIDE
from multiprocessing import Process, Queue, cpu_count
from pyproj import Proj
import math
import pyprind
import time
import traceback


def write_log_message(message):
    with open("matchstick.log", "a") as f:
        f.write(message + "\n")


def matchstick(status, output, from_map, from_objs, to_map, worker_id=None, verbose=True):
    """Multiprocessing worker for map matching"""
    if not from_objs:
        return

    if verbose:
        write_log_message("""Starting matchstick:
    from map: %s
    from objs: %s (%s to %s)
    to map: %s
    worker id: %s""" % (from_map, len(from_objs), min(from_objs), max(from_objs), to_map, worker_id))

    results = {}
    to_map = Map(to_map)

    if verbose:
        write_log_message("%s: Loaded to map. Vector? %s" % (worker_id, to_map.vector))

    rtree_index = to_map.create_rtree_index()
    from_map = Map(from_map)

    if verbose:
        write_log_message("%s: Loaded from map. Vector? %s" % (worker_id, from_map.vector))

    skip_projection = (from_map.crs == to_map.crs) or \
        (Proj(wgs84(from_map.crs)).is_latlong() and \
         Proj(wgs84(to_map.crs)).is_latlong())

    if from_objs:
        from_gen = zip(from_objs, from_map.select(from_objs))
    else:
        from_gen = enumerate(from_map)

    for task_index, (from_index, from_obj) in enumerate(from_gen):
        try:
            geom = to_shape(from_obj['geometry'])

            if not skip_projection:
                geom = project(geom, from_map.crs, to_map.crs)

            possibles = rtree_index.intersection(geom.bounds)
            for candidate_index in possibles:
                candidate = to_map[candidate_index]
                candidate_geom = to_shape(candidate['geometry'])
                if not geom.intersects(candidate_geom):
                    continue

                intersection = geom.intersection(candidate_geom)
                if not intersection.area:
                    continue

                results[(from_index, candidate_index)] = \
                    project(intersection, to_map.crs, MOLLWEIDE).area

            if status:
                status.put((worker_id, (task_index, len(from_objs or []))))

        except:
            if verbose:
                write_log_message(traceback.format_exc())
            raise

    output.put(results)
    return


def areal_calculation(status, output, from_map, from_objs, worker_id=None, verbose=True):
    """Multiprocessing worker for areas of each object in a map"""
    if not from_objs:
        return

    if verbose:
        write_log_message("""Starting areal calculation:
    from map: %s
    from objs: %s (%s to %s)
    worker id: %s""" % (from_map, len(from_objs), min(from_objs), max(from_objs), worker_id))

    results = {}

    from_map = Map(from_map)

    if verbose:
        write_log_message("%s: Loaded from map. Vector? %s" % (worker_id, from_map.vector))

    if from_objs:
        from_gen = zip(from_objs, from_map.select(from_objs))
    else:
        from_gen = enumerate(from_map)

    for task_index, (from_index, from_obj) in enumerate(from_gen):
        try:
            geom = to_shape(from_obj['geometry'])

            results[from_index] = \
                project(geom, from_map.crs, MOLLWEIDE).area

            if status:
                status.put((worker_id, (task_index, len(from_objs or []))))

        except:
            if verbose:
                write_log_message(traceback.format_exc())
            raise

    output.put(results)
    return


def matchmaker(from_map, to_map, from_objs=None, cpus=None):
    # Monitoring code adapted from
    # http://stackoverflow.com/questions/13689927/how-to-get-the-amount-of-work-left-to-be-done-by-a-python-multiprocessing-pool
    status = Queue()
    results_queue = Queue()

    if from_objs:
        map_size = len(from_objs)
        ids = from_objs
    else:
        map_size = len(Map(from_map))
        ids = range(map_size)

    cpus = min(cpus or cpu_count(), map_size + 0)

    chunk_size = int(math.ceil(map_size / float(cpus)))

    workers = []
    progress = {i: (0, 0) for i in range(cpus)}

    print("Starting pandarus job: \n\tworkers: %s\n\tmap size: %s\n\tchunk size: %s" % (cpus, map_size, chunk_size))

    if to_map is None:
        for index, chunk in enumerate([ids[i * chunk_size:(i + 1) * chunk_size]
                for i in range(cpus)]):

            child = Process(
                target=areal_calculation,
                args=(status, results_queue, from_map, chunk, index)
            )
            workers.append(child)
            child.start()
    else:
        for index, chunk in enumerate([ids[i * chunk_size:(i + 1) * chunk_size]
                for i in range(cpus)]):

            child = Process(
                target=matchstick,
                args=(status, results_queue, from_map, chunk, to_map, index)
            )
            workers.append(child)
            child.start()

    results=  {}

    bar = pyprind.ProgBar(map_size)

    while any(i.is_alive() for i in workers):
        time.sleep(0.1)
        while not status.empty():
            # Flush queue of progress reports
            worker, (completed, total) = status.get()
            progress[worker] = (completed, total)

        # Need to flush result queue or get weird multiprocessing errors
        # matchsticks finish but can't terminate correctly
        # see http://stackoverflow.com/questions/11854519/python-multiprocessing-some-functions-do-not-return-when-they-are-complete-que
        while not results_queue.empty():
            values = results_queue.get()
            if values:
                results.update(values)

        bar.update(sum([x[0] for x in progress.values()]))

    print(bar)

    while not results_queue.empty():
        values = results_queue.get()
        if values:
            results.update(values)

    return results
