import os
import math
from django.conf import settings
from osgeo import osr
from pysqlite2 import dbapi2 as db
from hashlib import md5
from collections import OrderedDict
import sh
import logging

from terrapyn.geocms import dispatch


_log = logging.getLogger('terrapyn.driver_messages')

CACHE_ROOT = getattr(settings, 'CACHE_ROOT', settings.MEDIA_ROOT)
LAYER_CACHE_PATH = getattr(settings, "LAYER_CACHE_PATH", os.path.join(CACHE_ROOT, '.cache', 'layers'))

if not os.path.exists(CACHE_ROOT):
    sh.mkdir('-p', CACHE_ROOT)

if not os.path.exists(LAYER_CACHE_PATH):
    sh.mkdir('-p', LAYER_CACHE_PATH)


def data_cache_path(page, page_id_field='slug'):
    """
    Get (and make) local data cache path for data
    :param page:
    :return:
    """
    path = os.path.join(CACHE_ROOT, '.cache', 'data', *os.path.split(getattr(page, page_id_field)))
    if not os.path.exists(path):
        sh.mkdir('-p', path)
    return path


def delete_data_cache(page, page_id_field='slug'):
    path = data_cache_path(page, page_id_field)
    sh.rm('-rf', path)


def trim_cache(layers=list(), styles=list()):
    """destroy relevant tile caches and cached mapnik files that are affected by a style or layer change"""
    names = []
    data = db.connect(os.path.join(LAYER_CACHE_PATH, 'directory.sqlite'))
    c = data.cursor()
    c.executemany('select basename from layers where slug=?', layers)
    names.extend( c.fetchall() )
    c.close()
    c = data.cursor()
    c.executemany('select basename from styles where slug=?', styles)
    names.extend( c.fetchall() )

    for name in names:
        if os.path.exists(name + '.mbtiles'):
            os.unlink(name + '.mbtiles')
        if os.path.exists(name + '.json'):
            os.unlink(name + '.json')
        if os.path.exists(name + '.wmsresults'):
            os.unlink(name + '.wmsresults')
        if os.path.exists(name + '.mml'):
            os.unlink(name + '.mml')
        if os.path.exists(name + '.xml'):
            os.unlink(name + '.xml')
        if os.path.exists(name + '.carto'):
            os.unlink(name + '.carto')


### following procedures and functions are in support of the tiled mapping services, TMS

def deg2num(lat_deg, lon_deg, zoom):
    """
    degree to tile number

    :param lat_deg: degrees lon
    :param lon_deg: degrees lat
    :param zoom: web mercator zoom level
    :return: x, y tile coordinates as a tuple
    """
    lat_rad = math.radians(lat_deg)
    n = 2.0 ** zoom
    xtile = int((lon_deg + 180.0) / 360.0 * n)
    ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n)
    return (xtile, ytile)


def num2deg(xtile, ytile, zoom):
    """
    Tile number to degree of southwest point.

    :param xtile: column
    :param ytile: row
    :param zoom: mercator zoom level
    :return: the degree of the southwest corner as a lat/lon pair.
    """
    n = 2.0 ** zoom
    lon_deg = xtile / n * 360.0 - 180.0
    lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
    lat_deg = math.degrees(lat_rad)
    return (lon_deg, lat_deg)


class MBTileCache(object):
    """
    MBTiles is MapBox's tile cache format.  We use it to store rendered tiles for display in a compact format.
    """

    STANDARD_SRS = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null"


    def __init__(self, layers, styles, bgcolor=None, transparent=True, query=None, srs=STANDARD_SRS, **kwargs):
        """
        Defines a cache that is specific to a set of layers, styles, spatial reference, and extra keyword args that
        are passed to the renderer.

        NOTE: currently only the standard SRS is supported.  This relies on the slippy map tile specification for
        geodetic coordinate to tile index location.

        :param layers: A list of layer identifier strings
        :param styles: A list of style identifier strings
        :param srs: A spatial reference system in proj.4 format. Typically (and by standard) Google Mercator
        :param kwargs: Extra keyword args to pass to the renderer
        """
        self.srs = srs
        self.name = CacheManager.cache_entry_name(layers, self.srs, styles, bgcolor, transparent, query)
        self.cachename = self.name + '.mbtiles'
        self.layers = layers if not isinstance(layers, basestring) else [layers]
        self.styles = styles if not isinstance(styles, basestring) else [styles]
        self.kwargs = kwargs

        # build a coordinate transformation based on the spatial reference passed in
        e4326 = osr.SpatialReference()
        e3857 = osr.SpatialReference()
        e4326.ImportFromEPSG(4326)
        e3857.ImportFromEPSG(3857)
        self.crx = osr.CoordinateTransformation(e4326, e3857)

        self.cache = self._ensure_cache_initted()

    def _ensure_cache_initted(self):
        if not os.path.exists(self.cachename):
            _log.info("Creating new tile cache for {0}".format(self.name))
            conn = db.connect(self.cachename)
            cursor = conn.cursor()
            cursor.executescript("""
                    BEGIN TRANSACTION;
                    CREATE TABLE android_metadata (locale text);
                    CREATE TABLE grid_key (grid_id TEXT,key_name TEXT);
                    CREATE TABLE grid_utfgrid (grid_id TEXT,grid_utfgrid BLOB);
                    CREATE TABLE keymap (key_name TEXT,key_json TEXT);
                    CREATE TABLE images (tile_data blob,tile_id text);
                    CREATE TABLE map
                        (zoom_level INTEGER,tile_column INTEGER,tile_row INTEGER,tile_id TEXT,grid_id TEXT);
                    CREATE TABLE metadata (name text,value text);
                    CREATE VIEW tiles
                        AS SELECT
                            map.zoom_level AS zoom_level,
                            map.tile_column AS tile_column,
                            map.tile_row AS tile_row,
                            images.tile_data AS tile_data
                            FROM map
                            JOIN images ON images.tile_id = map.tile_id
                            ORDER BY zoom_level,tile_column,tile_row;
                    CREATE VIEW grids
                        AS SELECT
                            map.zoom_level AS zoom_level,
                            map.tile_column AS tile_column,
                            map.tile_row AS tile_row,
                            grid_utfgrid.grid_utfgrid AS grid
                            FROM map
                            JOIN grid_utfgrid ON grid_utfgrid.grid_id = map.grid_id;
                    CREATE VIEW grid_data
                        AS SELECT
                            map.zoom_level AS zoom_level,
                            map.tile_column AS tile_column,
                            map.tile_row AS tile_row,
                            keymap.key_name AS key_name,
                            keymap.key_json AS key_json
                            FROM map
                            JOIN grid_key ON map.grid_id = grid_key.grid_id
                            JOIN keymap ON grid_key.key_name = keymap.key_name;
                    CREATE UNIQUE INDEX grid_key_lookup ON grid_key (grid_id,key_name);
                    CREATE UNIQUE INDEX grid_utfgrid_lookup ON grid_utfgrid (grid_id);
                    CREATE UNIQUE INDEX keymap_lookup ON keymap (key_name);
                    CREATE UNIQUE INDEX images_id ON images (tile_id);
                    CREATE UNIQUE INDEX map_index ON map (zoom_level, tile_column, tile_row);
                    CREATE UNIQUE INDEX name ON metadata (name);
                    END TRANSACTION;
                    ANALYZE;
                    VACUUM;
               """)
            cursor.close()
            return conn
        else:
            return db.connect(self.cachename)

    def fetch_tile(self, z, x, y):
        """
        Fetch map by slippy map tile ID.  Render it if it's not rendered already.

        :param z: zoom level
        :param x: x
        :param y: y
        :return:
        """
        tile_id = u':'.join(str(k) for k in (z,x,y))
        sw = self.crx.TransformPoint(*num2deg(x, y+1, z))
        ne = self.crx.TransformPoint(*num2deg(x+1, y, z))
        width = 256
        height = 256
        insert_map = "INSERT OR REPLACE INTO map (tile_id,zoom_level,tile_column,tile_row,grid_id) VALUES(?,?,?,?,'');"
        insert_data = "INSERT OR REPLACE INTO images (tile_id,tile_data) VALUES(?,?);"

        c = self.cache.cursor()
        c.execute("SELECT tile_data FROM images WHERE tile_id=?", [tile_id])
        try:
            rendered = False
            blob = buffer(c.fetchone()[0])
        except:
            rendered = True
            dispatch.tile_rendered.send(sender=CacheManager, layers=self.layers, styles=self.styles)
            if getattr(settings, 'USE_CELERY', False):
                from terrapyn.geocms.tasks import render as delayed_render
                blob = delayed_render.delay(
                    'png', width, height,
                    (sw[0], sw[1], ne[0], ne[1]), self.srs, self.styles, self.layers, **self.kwargs).get()
            else:
                from terrapyn.geocms.rendering import Renderer
                filename, blob = Renderer().render(
                    'png',
                    width,
                    height,
                    (sw[0], sw[1], ne[0], ne[1]),
                    self.srs,
                    self.styles,
                    self.layers,
                    **self.kwargs)

            if len(blob) > 350:
                blob = buffer(blob)
                d = self.cache.cursor()
                d.execute(insert_map, [tile_id, z, x, y])
                d.execute(insert_data, [tile_id, blob])
                self.cache.commit()
                d.close()
        c.close()

        return rendered, blob

    def seed_tiles(self, min_zoom, max_zoom, minx, miny, maxx, maxy):
        """
        Force rendering of tiles for an area.

        :param min_zoom:
        :param max_zoom:
        :param minx:
        :param miny:
        :param maxx:
        :param maxy:
        :return:
        """
        for z in range(min_zoom, max_zoom+1):
            mnx, mny = deg2num(miny, minx, z)
            mxx, mxy = deg2num(maxy, maxx, z)
            for x in range(mnx, mxx+1):
                for y in range(mny, mxy+1):
                    self.fetch_tile(z, x, y)

    @classmethod
    def shave_cache(cls, filename, bbox):
        """
        Empties a bounding box out of the cache at all zoom levels to be regenerated on demand.  For supporting
        minor edits on data.

        :param filename:
        :param bbox:
        :return:
        """

        x1, y1, x2, y2 = bbox
        conn = db.connect(filename)
        c = conn.cursor()
        c.execute('select min(zoom_level) from map')
        c.execute('select max(zoom_level) from map')
        min_zoom = c.fetchone()
        max_zoom = c.fetchone()

        if min_zoom:
            min_zoom = min_zoom[0]
        else:
            min_zoom = 0
        if max_zoom:
            max_zoom = max_zoom[0]
        else:
            max_zoom = 32

        c.close()

        c = conn.cursor()

        del_map_entry = """
        DELETE FROM map WHERE
            tile_column >= ? AND
            tile_row >= ? AND
            tile_column <= ? AND
            tile_row <= ? AND
            zoom_level = ?
        """

        del_tile_data = """
        DELETE FROM images
        WHERE tile_id IN (
            SELECT tile_id
            FROM map WHERE
                tile_column >= ? AND
                tile_row >= ? AND
                tile_column <= ? AND
                tile_row <= ? AND
                zoom_level = ?
        )
        """
        e4326 = osr.SpatialReference()
        e3857 = osr.SpatialReference()
        e4326.ImportFromEPSG(4326)
        e3857.ImportFromEPSG(3857)
        crx = osr.CoordinateTransformation(e3857, e4326)
        x1, y1, _ = crx.TransformPoint(x1, y1)
        x2, y2, _ = crx.TransformPoint(x2, y2)


        for zoom in range(min_zoom, max_zoom+1):
            a1, b1 = deg2num(y1, x1, zoom)
            a2, b2 = deg2num(y2, x2, zoom)
            c.execute(del_tile_data, [a1, b1, a2, b2, zoom])
            c.execute(del_map_entry, [a1, b1, a2, b2, zoom])

        c.execute('ANALYZE')
        c.execute('VACUUM')

        conn.commit()
        conn.close()


class WMSResultsCache(object):
    """
    Cache for rendered WMS results.
    """
    def __init__(self, layers, srs, styles, **kwargs):
        self.name = CacheManager.cache_entry_name(
            layers, srs, styles,
            bgcolor=kwargs.get('bgcolor', None),
            transparent=kwargs.get('transparent', False),
            query=kwargs.get('query', None)
        )
        self.cachename = self.name + '.wmscache'

        self.srs = srs
        self.layers = layers
        self.styles = styles
        self.kwargs = kwargs

        if os.path.exists(self.cachename):
            conn = db.connect(self.cachename)
            conn.enable_load_extension(True)
            conn.execute("select load_extension('libspatialite.so')")
        else: