# -*- coding: UTF-8 -*-
import logging
import math
import os

import arcpy

from pyspatialopt import version


def generate_query(unique_ids, unique_field_name, wrap_values_in_quotes=False):
    """
    Generates a select or definition query that can applied to the input layers
    :param unique_ids: (list) A list of ids to query
    :param unique_field_name: (string) The name of field that the ids correspond to
    :param wrap_values_in_quotes: (bool) Should the ids be wrapped in quotes (if unique_field_name is string)
    :return: (string) A query string that can be applied to a layer
    """
    if unique_ids:
        if wrap_values_in_quotes:
            query = "{} in (-1,{})".format(unique_field_name, ",".join("'{0}'".format(w) for w in unique_ids))
        else:
            query = "{} in (-1,{})".format(unique_field_name, ",".join(unique_ids))
    else:
        query = "{} in (-1)".format(unique_field_name)
    return query


def reset_layers(*args):
    """
    Clears the selection and definition query applied to the layers
    :param args: (Feature Layers) The feature layers to reset
    :return:
    """
    for layer in args:
        arcpy.SelectLayerByAttribute_management(layer, "CLEAR_SELECTION")
        layer.definitionQuery = ""


def generate_serviceable_demand(dl, dl_demand_field, dl_id_field, *args):
    """
    Finds to total serviceable coverage when 2 facility layers are used
    Merges polygons & dissolves them to form one big area of total coverage
    Then intersects with demand layer. Only used for partial coverages
    :param dl: (Feature Layer) The demand polygon or point layer
    :param dl_demand_field: (string) The field representing demand
    :param dl_id_field: (string) The name of the unique field for the demand layer
    :param args: (Feature Layer) The facility layers to use
    :return: (dictionary) A dictionary of similar format to the coverage format
    """
    # Reset DF
    # Check parameters so we get useful exceptions and messages
    reset_layers(dl)
    reset_layers(*args)
    if arcpy.Describe(dl).shapeType not in ["Polygon", "Point"]:
        raise TypeError("Demand layer must have polygon geometry")
    dl_field_names = [f.name for f in arcpy.Describe(dl).fields]
    if dl_demand_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_demand_field))
    if dl_id_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_id_field))
    # Check that all facility layers are polygon
    for fl in args:
        if arcpy.Describe(fl).shapeType != "Polygon":
            raise TypeError("{} is not a polygon layer".format(fl.desc.name))
    if fl is None:
        raise ValueError("No facility service area feature layers specified")
    dl_desc = arcpy.Describe(dl)
    logging.getLogger().info("Initializing output...")
    if dl_desc.shapeType == "Polygon":
        output = {
            "version": version.__version__,
            "demand": {},
            "type": {
                "mode": "serviceableDemand",
                "type": "partial"}
        }
    elif dl_desc.shapeType == "Point":
        output = {
            "version": version.__version__,
            "demand": {},
            "type": {
                "mode": "serviceableDemand",
                "type": "binary"}
        }
    else:
        raise TypeError("Demand layer must be point or polygon")
    logging.getLogger().info("Combining facilities...")
    dissovled_geom = None
    for layer in args:
        with arcpy.da.SearchCursor(layer, ['SHAPE@']) as fcursor:
            for f in fcursor:
                if dissovled_geom is None:
                    dissovled_geom = f[0]
                dissovled_geom = dissovled_geom.union(f[0])
    logging.getLogger().info("Determining possible service coverage for each demand unit...")
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field, "SHAPE@"]) as dcursor:
        if arcpy.Describe(dl).shapeType == "Polygon":
            for d in dcursor:
                if not dissovled_geom.disjoint(d[2]):
                    intersected = dissovled_geom.intersect(d[2], 4)
                    if intersected.area > 0:
                        serviceable_demand = math.ceil(
                            float(intersected.area / d[2].area) * d[1])
                    else:
                        serviceable_demand = 0.0
                else:
                    serviceable_demand = 0.0
                # Make sure serviceable is less than or equal to demand, floating point issues
                if serviceable_demand < d[1]:
                    output["demand"][str(d[0])] = {"serviceableDemand": serviceable_demand}
                else:
                    output["demand"][str(d[0])] = {"serviceableDemand": d[1]}
        else:  # Point
            for d in dcursor:
                intersected = dissovled_geom.intersect(d[2], 1)
                if intersected.centroid:  # check if valid
                    serviceable_demand = d[1]
                else:
                    serviceable_demand = 0.0
                output["demand"][str(d[0])] = {"serviceableDemand": serviceable_demand}
    logging.getLogger().info("Serviceable demand successfully created.")
    reset_layers(dl)
    reset_layers(*args)
    return output


def generate_binary_coverage(dl, fl, dl_demand_field, dl_id_field, fl_id_field, fl_variable_name=None):
    """
    Generates a dictionary representing the binary coverage of a facility to demand points
    :param dl: (Feature Layer) The demand polygon or point layer
    :param fl: (Feature Layer) The facility service area polygon layer
    :param dl_demand_field: (string) The name of the field in the demand layer that describes the demand
    :param dl_id_field: (string) The name of the unique identifying field on the demand layer
    :param fl_id_field: (string) The name of the unique identifying field on the facility layer
    :param fl_variable_name: (string) The name to use to represent the facility variable
    :return: (dictionary) A nested dictionary storing the coverage relationships
    """
    # Check parameters so we get useful exceptions and messages
    if arcpy.Describe(dl).shapeType not in ["Polygon", "Point"]:
        raise TypeError("Demand layer must have polygon or point geometry")
    if arcpy.Describe(fl).shapeType != "Polygon":
        raise TypeError("Facility service area layer must have polygon geometry")
    dl_field_names = [f.name for f in arcpy.Describe(dl).fields]
    if dl_demand_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_demand_field))
    if dl_id_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_id_field))
    fl_field_names = [f.name for f in arcpy.Describe(fl).fields]
    if fl_id_field not in fl_field_names:
        raise ValueError("'{}' field not found in facility service area layer".format(fl_id_field))
    reset_layers(dl, fl)
    if fl_variable_name is None:
        fl_variable_name = os.path.splitext(os.path.basename(arcpy.Describe(fl).name))[0]
    logging.getLogger().info("Initializing facilities in output...")
    output = {
        "version": version.__version__,
        "type": {
            "mode": "coverage",
            "type": "binary",
        },
        "demand": {},
        "totalDemand": 0.0,
        "totalServiceableDemand": 0.0,
        "facilities": {fl_variable_name: []}
    }
    # List all of the facilities
    with arcpy.da.SearchCursor(fl, [fl_id_field]) as cursor:
        for row in cursor:
            output["facilities"][fl_variable_name].append(str(row[0]))
    # Build empty data structure
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field, "SHAPE@AREA"]) as cursor:
        for row in cursor:
            output["demand"][str(row[0])] = {
                "area": round(row[2]),
                "demand": round(row[1]),
                "serviceableDemand": 0,
                "coverage": {fl_variable_name: {}}
            }
    logging.getLogger().info("Determining binary coverage for each demand unit...")
    with arcpy.da.SearchCursor(fl, [fl_id_field, "SHAPE@"]) as fcursor:
        if arcpy.Describe(dl).shapeType == "Point":
            for f in fcursor:
                with arcpy.da.SearchCursor(dl, [dl_id_field, "SHAPE@"]) as dcursor:
                    for d in dcursor:
                        if not f[1].disjoint(d[1]):
                            output["demand"][str(d[0])]["serviceableDemand"] = \
                                output["demand"][str(d[0])]["demand"]
                            output["demand"][str(d[0])]["coverage"][fl_variable_name][str(f[0])] = 1
        else:  # Polygon
            for f in fcursor:
                with arcpy.da.SearchCursor(dl, [dl_id_field, "SHAPE@"]) as dcursor:
                    for d in dcursor:
                        if not f[1].disjoint(d[1]):
                            if f[1].contains(d[1]):
                                output["demand"][str(d[0])]["serviceableDemand"] = \
                                    output["demand"][str(d[0])]["demand"]
                                output["demand"][str(d[0])]["coverage"][fl_variable_name][str(f[0])] = 1
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field]) as cursor:
        for row in cursor:
            output["totalServiceableDemand"] += output["demand"][str(row[0])]["serviceableDemand"]
            output["totalDemand"] += row[1]
    logging.getLogger().info("Binary coverage successfully generated.")
    reset_layers(dl, fl)
    return output


def generate_partial_coverage(dl, fl, dl_demand_field, dl_id_field="OBJECTID", fl_id_field="OBJECTID",
                              fl_variable_name=None):
    """
    Generates a dictionary representing the partial coverage (based on area) of a facility to demand areas
    :param dl: (Feature Layer) The demand polygon layer
    :param fl: (Feature Layer) The facility service area polygon layer
    :param dl_demand_field: (string) The name of the field in the demand layer that describes the demand
    :param dl_id_field: (string) The name of the unique identifying field on the demand layer
    :param fl_id_field: (string) The name of the unique identifying field on the facility layer
    :param fl_variable_name: (string) The name to use to represent the facility variable
    :return: (dictionary) A nested dictionary storing the coverage relationships
    """
    # Reset DF
    # Check parameters so we get useful exceptions and messages
    if arcpy.Describe(dl).shapeType != "Polygon":
        raise TypeError("Demand layer must have polygon geometry")
    if arcpy.Describe(fl).shapeType != "Polygon":
        raise TypeError("Facility service area layer must have polygon geometry")
    dl_field_names = [f.name for f in arcpy.Describe(dl).fields]
    if dl_demand_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_demand_field))
    if dl_id_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_id_field))
    fl_field_names = [f.name for f in arcpy.Describe(fl).fields]
    if fl_id_field not in fl_field_names:
        raise ValueError("'{}' field not found in facility service area layer".format(fl_id_field))
    reset_layers(dl, fl)
    if fl_variable_name is None:
        fl_variable_name = os.path.splitext(os.path.basename(arcpy.Describe(fl).name))[0]
    # Create the initial data structure
    logging.getLogger().info("Initializing facilities in output...")
    output = {
        "version": version.__version__,
        "type": {
            "mode": "coverage",
            "type": "partial",
        },
        "demand": {},
        "totalDemand": 0.0,
        "totalServiceableDemand": 0.0,
        "facilities": {fl_variable_name: []}
    }
    # Populate the facility ids
    with arcpy.da.SearchCursor(fl, [fl_id_field]) as cursor:
        for row in cursor:
            output["facilities"][fl_variable_name].append(str(row[0]))
    # populate the coverage dictionary with all demand areas (i)
    logging.getLogger().info("Initializing demand in output...")
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field, "SHAPE@AREA"]) as cursor:
        for row in cursor:
            output["demand"][str(row[0])] = {
                "area": round(row[2]),
                "demand": round(row[1]),
                "serviceableDemand": 0.0,
                "coverage": {fl_variable_name: {}}
            }
    # Dissolve all facility service areas so we can find the total serviceable area
    logging.getLogger().info("Combining facilities...")
    dissovled_geom = None
    with arcpy.da.SearchCursor(fl, ['SHAPE@']) as fcursor:
        for f in fcursor:
            if dissovled_geom is None:
                dissovled_geom = f[0]
            dissovled_geom = dissovled_geom.union(f[0])
    logging.getLogger().info("Determining partial coverage for each demand unit...")
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field, "SHAPE@"]) as dcursor:
        for d in dcursor:
            if not dissovled_geom.disjoint(d[2]):
                intersected = dissovled_geom.intersect(d[2], 4)
                if intersected.area > 0:
                    serviceable_demand = math.ceil(
                        float(intersected.area / d[2].area) * d[1])
                else:
                    serviceable_demand = 0.0
            else:
                serviceable_demand = 0.0
            # Make sure serviceable is less than or equal to demand, floating point issues
            if serviceable_demand < output["demand"][str(d[0])]["demand"]:
                output["demand"][str(d[0])]["serviceableDemand"] = serviceable_demand
            else:
                output["demand"][str(d[0])]["serviceableDemand"] = output["demand"][str(d[0])]["demand"]
            with arcpy.da.SearchCursor(fl, [fl_id_field, "SHAPE@"]) as fcursor:
                for f in fcursor:
                    if not d[2].disjoint(f[1]):
                        intersected_fd = d[2].intersect(f[1], 4)
                        if intersected_fd.area > 0:
                            demand = math.ceil(float(intersected_fd.area / d[2].area) * d[1])
                            if demand < output["demand"][str(d[0])]["serviceableDemand"]:
                                output["demand"][str(d[0])]["coverage"][fl_variable_name][str(f[0])] = demand
                            else:
                                output["demand"][str(d[0])]["coverage"][fl_variable_name][str(f[0])] = \
                                    output["demand"][str(d[0])]["serviceableDemand"]
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field, "SHAPE@AREA"]) as cursor:
        for row in cursor:
            output["totalServiceableDemand"] += output["demand"][str(row[0])]["serviceableDemand"]
            output["totalDemand"] += row[1]
    logging.getLogger().info("Partial coverage successfully generated.")
    reset_layers(dl, fl)
    return output

def generate_traumah_coverage(dl, dl_service_area, tc_layer, ad_layer, dl_demand_field, air_distance_threshold, dl_id_field="OBJECTID", tc_layer_id_field="OBJECTID", ad_layer_id_field="OBJECTID"):
    """
    Generates a coverage model for the TRAUMAH model. The traumah model uses trauma centers (TC), air depots (AD), and demand
    :param dl: (Feature Layer) The demand point layer
    :param dl_service_area (Feature Layer) The demand service area (generally derived from street network)
    :param tc_layer: (Feature Layer) The Trauma Center point layer
    :param ad_layer: (Feature Layer) The Air Depot point layer
    :param dl_demand_field: (string) The attribute that represents the demand in the demand layer
    :param air_distance_threshold: (float) The maximum total distance a helicopter can fly
    :param dl_id_field: (string) The attribute that represents unique ids for the demand layers
    :param tc_layer_id_field: (string) The attribute that represents unique ids for the trauma center layers
    :param ad_layer_id_field: (string) The attribute that represents unique ids for the air depot layers
    :return: (dictionary) A nested dictionary storing the coverage relationships
    """
    # Reset DF
    # Check parameters so we get useful exceptions and messages
    if arcpy.Describe(dl).shapeType != "Point":
        raise TypeError("Demand layer must have point geometry")
    if arcpy.Describe(dl_service_area).shapeType != "Polygon":
        raise TypeError("Demand layer must have polygon geometry")
    if arcpy.Describe(tc_layer).shapeType != "Point":
        raise TypeError("Trauma center layer must have point geometry")
    dl_field_names = [f.name for f in arcpy.Describe(dl).fields]
    dl_service_area_field_names = [f.name for f in arcpy.Describe(dl_service_area).fields]
    if dl_demand_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_demand_field))
    if dl_id_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_id_field))
    if dl_id_field not in dl_service_area_field_names:
        raise ValueError("'{}' field not found in demand service area layer".format(dl_id_field))
    tc_layer_field_names = [f.name for f in arcpy.Describe(tc_layer).fields]
    if tc_layer_id_field not in tc_layer_field_names:
        raise ValueError("'{}' field not found in trauma center layer".format(tc_layer_id_field))
    ad_layer_field_names = [f.name for f in arcpy.Describe(ad_layer).fields]
    if ad_layer_id_field not in ad_layer_field_names:
        raise ValueError("'{}' field not found in air depot layer".format(ad_layer_id_field))
    reset_layers(dl, dl_service_area, tc_layer, ad_layer)
    ad_variable_name = "AirDepot"
    tc_variable_name = "TraumaCenter"
    ad_tc_variable_name = "ADTCPair"

    logging.getLogger().info("Initializing facilities in output...")
    output = {
        "version": version.__version__,
        "type": {
            "mode": "coverage",
            "type": "traumah",
        },
        "demand": {},
        "totalDemand": 0.0,
        "totalServiceableDemand": 0.0,
        "facilities": {ad_variable_name: [],
                       tc_variable_name: []}
    }
    # Populate the facility ids
    with arcpy.da.SearchCursor(ad_layer, [ad_layer_id_field]) as cursor:
        for row in cursor:
            output["facilities"][ad_variable_name].append(str(row[0]))
    with arcpy.da.SearchCursor(tc_layer, [tc_layer_id_field]) as cursor:
        for row in cursor:
            output["facilities"][tc_variable_name].append(str(row[0]))
    # populate the coverage dictionary with all demand areas (i)
    logging.getLogger().info("Initializing demand in output...")
    with arcpy.da.SearchCursor(dl, [dl_id_field, dl_demand_field, "SHAPE@AREA"]) as cursor:
        for row in cursor:
            output["demand"][str(row[0])] = {
                "area": round(row[2]),
                "demand": round(row[1]),
                "serviceableDemand": 0.0,
                "coverage": {tc_variable_name: [],
                             ad_tc_variable_name: []
                }
            }
    logging.getLogger().info("Determining binary coverage (using ground transport service area) for each demand unit...")
    with arcpy.da.SearchCursor(tc_layer, [tc_layer_id_field, "SHAPE@"]) as fcursor:
        for f in fcursor:
            with arcpy.da.SearchCursor(dl_service_area, [dl_id_field, "SHAPE@"]) as dcursor:
                for d in dcursor:
                    if not f[1].disjoint(d[1]):
                        output["demand"][str(d[0])]["coverage"][tc_variable_name].append({
                            tc_variable_name: str(f[0])
                        })

    logging.getLogger().info("Determining binary coverage (using air transportation) for each demand unit...")
    with arcpy.da.SearchCursor(dl, [dl_id_field, "SHAPE@"]) as dcursor:
        for d in dcursor:
            distances = {}
            with arcpy.da.SearchCursor(tc_layer, [tc_layer_id_field, "SHAPE@"]) as tc_cursor:
                for tc in tc_cursor:
                    distances[tc[0]] = d[1].distanceTo(tc[1])

            with arcpy.da.SearchCursor(ad_layer, [ad_layer_id_field, "SHAPE@"]) as ad_cursor:
                for ad in ad_cursor:
                    distance = d[1].distanceTo(ad[1])
                    for k, v in distances.items():
                         if distance+v <= air_distance_threshold:
                             output["demand"][str(d[0])]["coverage"][ad_tc_variable_name].append({
                                 tc_variable_name: str(k),
                                 ad_variable_name: str(ad[0])
                             })
    logging.getLogger().info("Binary traumah coverage successfully generated.")
    reset_layers(dl, tc_layer, ad_layer)
    return output


def get_covered_demand(dl, dl_demand_field, mode, *args):
    """
    Finds to total coverage when facility layers are used
    Merges polygons & dissolves them to form one big area of total coverage
    Then intersects with demand layer. Only used for partial coverages

    Honors definition query and selection for facility layers
    :param dl: (Feature Layer) The demand polygon or point layer
    :param dl_demand_field: (string) The field representing demand
    :param mode: (string) ['binary', 'partial'] The method to use to evaluate coverage
    :param args: (Feature Layer) The facility layers to use
    :return: (dictionary) A dictionary of similar format to the coverage format
    """
    # Reset DF
    # Check parameters so we get useful exceptions and messages
    reset_layers(dl)
    if arcpy.Describe(dl).shapeType not in ["Polygon", "Point"]:
        raise TypeError("Demand layer must have polygon geometry")
    dl_field_names = [f.name for f in arcpy.Describe(dl).fields]
    if dl_demand_field not in dl_field_names:
        raise ValueError("'{}' field not found in demand layer".format(dl_demand_field))
    # Check that all facility layers are polygon
    for fl in args:
        if arcpy.Describe(fl).shapeType != "Polygon":
            raise TypeError("{} is not a polygon layer".format(fl.desc.name))
    if fl is None:
        raise ValueError("No facility service area feature layers specified")
    logging.getLogger().info("Combining facilities...")
    dissovled_geom = None
    for layer in args:
        with arcpy.da.SearchCursor(layer, ['SHAPE@']) as fcursor:
            for f in fcursor:
                if dissovled_geom is None:
                    dissovled_geom = f[0]
                dissovled_geom = dissovled_geom.union(f[0])
    total_coverage = 0
    logging.getLogger().info("Summing service coverage for each demand unit...")
    with arcpy.da.SearchCursor(dl, [dl_demand_field, "SHAPE@"]) as dcursor:
        if arcpy.Describe(dl).shapeType == "Polygon" and mode == "partial":
            for d in dcursor:
                if not dissovled_geom.disjoint(d[1]):
                    intersected = dissovled_geom.intersect(d[1], 4)
                    if intersected.area > 0:
                        serviceable_demand = math.ceil(
                            float(intersected.area / d[1].area) * d[0])
                    else:
                        serviceable_demand = 0.0
                else:
                    serviceable_demand = 0.0
                # Make sure serviceable is less than or equal to demand, floating point issues
                if serviceable_demand < d[0]:
                    total_coverage += serviceable_demand
                else:
                    total_coverage += d[0]
        else:  # binary point or polygon
            for d in dcursor:
                if dissovled_geom.contains(d[1]):  # check if valid
                    serviceable_demand = d[0]
                else:
                    serviceable_demand = 0.0
                total_coverage += serviceable_demand
    logging.getLogger().info("Covered demand is: {}".format(total_coverage))
    reset_layers(dl)
    return total_coverage