'''
Created on 10-Jul-2020

@author: Rishi Priyansh
'''
import re
import pathlib
from pathlib import Path
import os
import sys
import time
import random
import traceback
import csv
import inspect
import matplotlib as mpl
import matplotlib.pyplot as plt
from celluloid import Camera
from xvgui import _Base
from enum import Enum
from abc import ABC, abstractmethod
import geopandas
import numpy as np
import pandas as pd

import matplotlib.patches as mpatches



class GeoPlotting(_Base):
    class Type(Enum):
        CONTINENT = "CONTINENT"
        COUNTRY = "COUNTRY"
        CITY = "CITY"

    def __init__(self, **args):
        super().__init__(**args)
        self._items = []
        self.world = None
        self.cities = None
        self._loadData()
        self.edgecolor = (0.75, 0.75, 0.75)

        pass
    #def __init__(self, verbose = False):


    def find(self, *searchStrings):
        return self.search(*searchStrings)

    def search(self, *searchStrings):
        continents = self._search(GeoPlotting.Type.CONTINENT , *searchStrings)
        countries = self._search(GeoPlotting.Type.COUNTRY, *searchStrings)
        cities = self._search(GeoPlotting.Type.CITY, *searchStrings)

        found = {}

        if continents:
            found[GeoPlotting.Type.CONTINENT.value] = continents

        if countries:
            found[GeoPlotting.Type.COUNTRY.value] = countries

        if cities:
            found[GeoPlotting.Type.CITY.value] = cities

        return found

        pass
    #def find(self, search = None):

    def draw(self, *searchStrings):
        return self.plot(*searchStrings)
        pass

    def plot(self, *searchStrings):
        found = self.find(*searchStrings)

        if not found:
            print('no data found')
            return None

        return self._plot(found)
        pass
    #def plot(self, cityNames = None):

    def _plot(self, found):
        if not found:
            print('no data found')
            return None

        showPlot = False
        fig, ax, showPlot = self._initPlot()

        legends = []
        legendColors = []
        for geoShapeType, geoShapeNames in found.items():

            #print(shapeType, ':', geoShapes)
            for geoShapeName in geoShapeNames:

                geoShape = self.getShape(geoShapeType, geoShapeName)

                if geoShape.empty:
                    print("Error: No shape found for {}:{}".format(geoShapeType, geoShapeName))
                    continue

                centroid = self._getCentroid(geoShape)

                color = self._getRandomColor()
                labelColor = self._getContrastColor(color)

                geoShape.plot(ax = ax, color = color, edgecolor = self.edgecolor )

                if not geoShapeType == GeoPlotting.Type.CONTINENT.value:
                    ax.annotate(geoShapeName, # this is the text
                     centroid, # this is the point to label
                     textcoords="offset points", # how to position the text
                     xytext=(0,10), # distance from text to points (x,y)
                     ha='center',
                     color = labelColor
                     )
                else:
                    legends.append(mpatches.Patch(color = color, label = geoShapeName))

            pass
        #for geoShapeType, geoShapeNames in found.items():

        if legends:
            plt.legend(handles = legends, loc='best',  fontsize = 15)


        if showPlot:
            plt.show()

        pass
    #def plot(self, cityNames = None):

    def getShape(self, geoShapeType, geoShapeName):
        if  geoShapeType == GeoPlotting.Type.CONTINENT.value:
            geoShape = self.world[self.world['continent'] == geoShapeName]
            return geoShape
            pass

        if  geoShapeType == GeoPlotting.Type.COUNTRY.value:
            geoShape = self.world[self.world['name'] == geoShapeName]
            return geoShape
            pass

        if  geoShapeType == GeoPlotting.Type.CITY.value:
            return self.cities[self.cities['name'] == geoShapeName]
            pass

        return geopandas.GeoDataFrame()
        pass


    def plotNeighbors(self, searchString):
        found = self.find(searchString)

        if not found:
            print('no data found')
            return None

        if len(found) > 1:
            print("Too many records found")
            print(", ".join(found))
            return None

        geoShapeType = list(found.keys())[0]
        geoShapeNames = found[geoShapeType]

        if len(geoShapeNames) > 1:
            print("Too many shapes found")
            print(", ".join(geoShapeNames))
            return None

        geoShapeName = geoShapeNames[0]

        print("geoShapeType, geoShapeNames: ", geoShapeType, geoShapeName)
        geoShape = self.getShape(geoShapeType, geoShapeName)

        neighbors = self.getNeighbors(geoShapeName, geoShapeType, geoShape)

        print("neighbors: ", neighbors)

        return self._plot(neighbors)

        pass
    #def plot(self, cityNames = None):


    def getNeighbors(self, geoShapeName, geoShapeType, geoShape):
        '''
        This works only for the country
        '''

        df = None
        if  geoShapeType == GeoPlotting.Type.CONTINENT.value:
            return geoShape

        if  geoShapeType == GeoPlotting.Type.COUNTRY.value:
            df  = self.world
            pass

        if  geoShapeType == GeoPlotting.Type.CITY.value:
            return geoShape
            pass

        neighborIndexes =[]
        geoShapeNames = [geoShapeName]

        for ind1 in df.index:
            if self._touches(df.iloc[ind1], geoShape):
                neighborIndexes.append(ind1)
                geoShapeNames.append(df.iloc[ind1]['name'])
            pass

        return {geoShapeType: geoShapeNames}
        pass



    def _touches(self, shape1, shape2):

        p1 = self._getPolygon(shape1)
        p2 = self._getPolygon(shape2)

        for part1 in p1:
            for part2 in p2:
                if part1.touches(part2):
                    return True


        return False
        pass

    def _getPolygon(self, shape):
        #print("type(shape).__name__: '{}'".format(type(shape).__name__))

        if type(shape).__name__ == 'Polygon':
            return [shape]

        if type(shape.geometry).__name__ == 'Polygon':
            return [shape.geometry]

        return list(shape.geometry) #list(shape.geometry.items())[0][1]
        pass


    def _getCentroid(self, geoShape):

        centroids = geoShape.geometry.centroid

        if type(centroids).__name__ == 'Point':
            return (centroids.x, centroids.y)

        if len(centroids) ==  1:
            return (centroids.x, centroids.y)

        x = 0
        y = 0
        for centroid in centroids:
            x = x + centroid.x
            y = y + centroid.y

        x = x / len(centroids)
        y = y / len(centroids)
        return (x, y)

        pass
    #def _getCentroid(self, geoShape):

    def _search(self, dataType = None, *searchStrings):
        if not dataType:
            return None

        fullList  = None
        if  dataType == GeoPlotting.Type.CONTINENT:
            fullList  = list(self.world.continent.unique())
            pass

        if  dataType == GeoPlotting.Type.COUNTRY:
            fullList  = list(self.world.name.unique())
            pass

        if  dataType == GeoPlotting.Type.CITY:
            fullList  = list(self.cities.name.unique())
            pass

        if not searchStrings:
            return fullList

        #continents = [x for x in list(self.world.continent.unique()) if continentName.lower() in x.lower()]
        found = [x for x in fullList
                      for searchString in searchStrings
                      if searchString.lower() in x.lower()]

        if not found:
            print('No matching {} found'.format(dataType))
            return None

        found = sorted(list(set(found)))

        print("{}: {}".format(dataType.value , found))
        return found

        pass
    #def _search(self, dataType = None, *searchStrings):


    def _loadData(self):
        self.world =  geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
        self.cities = geopandas.read_file(geopandas.datasets.get_path('naturalearth_cities'))

        pass
    #def _loadData(self):

    def _initPlot(self):
        fig, ax = plt.subplots(figsize=(15, 8), dpi=120)
        ax.set_aspect('equal')
        showPlot = True
        return (fig, ax, showPlot)
        pass
    #def initPlot(self):

    def _getRandomColor(self):
        startFrom = 0.75
        r = random.uniform(startFrom, 1)
        g = random.uniform(startFrom, 1)
        b = random.uniform(startFrom, 1)
        return (r, g, b)
        pass
    #def _getRandomColor(self):

    def _getContrastColor(self, color):
        return (1 - color[0], 1 - color[1], 1 - color[2])
        pass
    #def _getContrastColor(self):




    def __str__(self):
        s = "GeoPlotting:"
        s = s + "\n" + "You can plot world, continent or city maps."

        continents = ", ".join(sorted(list(self.world.continent.unique())))
        s = s + "\n\n" + "Continents: " + "\n" + continents

        countries = ", ".join(sorted(list(self.world.name.unique())))
        s = s + "\n\n" + "Countries: " + "\n" + countries

        cities = ", ".join(sorted(list(self.cities.name.unique())))
        s = s + "\n\n" + "Cities: " + "\n" + cities

        return s
        pass

    pass
#class GeoPlotting(_Base):

class _TEST_GeoPlotting:
    @classmethod
    def test1(cls):

        gp = GeoPlotting()

        countries = gp.findCountry()
        print(countries)

        print("\nFunction {} over\n#############\n".format(inspect.stack()[0][3]))
        pass
    # def test1(cls):


    pass
# class _TEST_GeoPlotting:


if __name__ == "__main__":
    _TEST_GeoPlotting.test1()