# !/usr/bin/env python
# encoding=utf-8

import json
import random
import functools
import pickle
import Geohash
import copy
from math import radians, cos, sin, asin, sqrt

import requests
from django.conf import settings
from gm_types.gaia import CONST_STRINGS
from api.models import City, Province
from rpc.cache import qq_city_cache, city_info_cache
from rpc.tool.log_tool import logging_exception, locate_city_logger
from api.tool.geo_qq_gm_map import gm_qq_province_map

QQ_MAP_API_URL = 'https://apis.map.qq.com/ws/geocoder/v1'
QQ_HTTP_TIMEOUT = 5.0          # seconds
QQ_POS2NAME_CACHE_TIME = 1200  # seconds
QQ_POS2NAME_CACHE_TIME_RANGE = 600
CITY_CACHE_TIME = 3600         # seconds
GEO_HASH_LENGTH = 5

class degrade_strategy(object):
    def __init__(self, level=0):
        self.level = level

    def __get__(self, instance, owner):
        # todo get global level
        return self.level

    def __set__(self, instance, value):
        self.level = value



class cache_wrap(object):
    level = degrade_strategy()
    def __init__(self, cache_serve, pre_fix='', expire=3600, default_level=0, step=2, key_func=None, time_range=600):
        self.cache_serve = cache_serve
        self.pre_fix = pre_fix
        self._expire = expire
        self.default_level = default_level
        self.step = step
        self.key_func = key_func or (lambda *args: ':'.join([str(a) for a in args]))
        self.time_range = time_range

    @property
    def expire(self):
        level = self.level - self.default_level
        if level >= 0:
            return self._expire * (self.step ** level)
        else:
            return 0

    @property
    def random_expire(self):
        expire = self.expire
        if expire > 0:
            if self.time_range:
                return expire + random.randint(0, self.time_range)
            return expire
        return 0

    def _set_cache(self, key, value):
        value = pickle.dumps(value)
        if self.cached:
            self.cache_serve.setex(key, self.random_expire, value)

    def _get_cache(self, key):
        _get = False
        value = self.cache_serve.get(key)
        if value:
            _get = True
            try:
                value = pickle.loads(value)
            except:
                pass
        return _get, value

    def _mset_cache(self, arg_params):
        for k, v in arg_params.items():
            d_v = pickle.dumps(v)
            f_k = self._get_cache_key([f_k])
            self.cache_serve.setex(f_k, self.random_expire, d_v)

    def _mget_cache(self, keys):
        value = self.cache_serve.mget(keys)
        return value

    @property
    def cached(self):
        return self.expire > 0

    def _get_cache_key(self, *args, **kwargs):
        t_args = list(copy.deepcopy(args))
        t_kwargs = copy.deepcopy(kwargs)
        key = self.key_func(*t_args, **t_kwargs)
        return ':'.join([str(self.pre_fix), key])

    def __call__(self, func):
        @functools.wraps(func)
        def wrapped_func(*args, **kwargs):
            key = self._get_cache_key(*args, **kwargs)
            if self.cached:
                _get, value = self._get_cache(key)
                if value:
                    return value
            value = func(*args)
            if self.cached and value:
                self._set_cache(key, value)
            return value

        return wrapped_func

class qq_location(object):
    level = degrade_strategy()
    API_URL = QQ_MAP_API_URL
    def __init__(self, timeout=QQ_HTTP_TIMEOUT, default_level=0, step=0.5):
        self._timeout = timeout
        self.default_level = default_level
        self.step = step
        self.verify = False

    @property
    def timeout(self):
        level = self.level - self.default_level
        return self._timeout * (self.step ** level)

    def _get_city_info_by_pos(self, lat, lng):

        params = {
            'location': '{},{}'.format(lat, lng),
            'get_poi': 0,
            'output': 'json',
            'key': random.choice(settings.QQ_MAP_KEYS)
        }

        res = {}

        try:
            response = requests.get(self.API_URL, params=params, timeout=self.timeout, verify=self.verify)
            result = response.json()
            if result['status'] == 0:
                address_component = result['result']['address_component']
                ad_info = result['result'].get('ad_info', {})

                city_name = None
                if address_component and ('city' in address_component or 'locality' in address_component):
                    city_name = address_component.get('city', '').replace(u'市', '')
                    if not city_name:
                        city_name = address_component.get('locality', '').replace(u"市", '')

                res["city_name"] = city_name
                res["nation_code"] = ad_info.get("nation_code")
                res["adcode"] = ad_info.get("adcode")
                res["city_code"] = ad_info.get("city_code")
                res["city"] = ad_info.get("city")
                res["province"] = ad_info.get("province")

        except Exception:
            logging_exception()

        return res

    #@cache_wrap(qq_city_cache, pre_fix='pos2name', default_level=0, expire=3600)
    def _get_name_by_pos(self, lat, lng):
        '''
        获取坐标获取城市名称

        :param lat: 纬度
        :param lng: 经度
        @return: str city_name
        '''

        params = {
            'location': '{},{}'.format(lat, lng),
            'get_poi': 0,
            'output': 'json',
            'key': random.choice(settings.QQ_MAP_KEYS)
        }

        city_name = None
        try:
            response = requests.get(self.API_URL, params=params, timeout=self.timeout, verify=self.verify)
            result = response.json()
            if result['status'] == 0:
                address_component = result['result']['address_component']
                if address_component and ('city' in address_component or 'locality' in address_component):
                    city_name = address_component.get('city', '').replace(u'市', '')
                    if not city_name:
                        city_name = address_component.get('locality', '').replace(u"市", '')
        except Exception:
            logging_exception()

        return city_name

    def get_pos_by_name(self, address):
        """由地址获获取经纬度坐标

        :param address: 目标地址 '西城区西什库大街8号(近地安门西大街)
        :return: 经纬度信息 {"lng": 116.306735, "lat": 39.982951}
        """
        params = {
            'address': address,
            'key': random.choice(settings.QQ_MAP_KEYS),
            'output': 'json',
        }
        response = requests.get(self.API_URL, params=params, verify=self.verify)
        result = response.json()
        if result['status'] == 0:
            return result['result']['location']
        else:
            return None

    def __str__(self):
        return self.__class__.__name__


def _get_geohash(lat, lon, length=GEO_HASH_LENGTH):
    return Geohash.encode(float(lat), float(lon), precision=length)

qq_city_manager = qq_location()
_get_city_name_by_pos = cache_wrap(qq_city_cache, pre_fix='pos2name', expire=QQ_POS2NAME_CACHE_TIME, key_func=lambda *args, **kwargs: _get_geohash(*args), time_range=QQ_POS2NAME_CACHE_TIME_RANGE)(qq_city_manager._get_name_by_pos)
_get_city_info_by_pos = cache_wrap(qq_city_cache, pre_fix='pos2city_info', expire=QQ_POS2NAME_CACHE_TIME, key_func=lambda *args, **kwargs: _get_geohash(*args), time_range=QQ_POS2NAME_CACHE_TIME_RANGE)(qq_city_manager._get_city_info_by_pos)

@cache_wrap(city_info_cache, pre_fix='city_name', expire=CITY_CACHE_TIME, key_func=lambda *args, **kwargs: ':'.join([str(arg.encode('utf-8') if isinstance(arg, unicode) else arg) for arg in args]))
def _get_city_by_name(city_name):
    """
    根据城市名返回City对象
    :param : city_name: 城市名
    :return: City对象
    """
    city = None
    try:
        city = City.objects.get(name=city_name)
    except City.DoesNotExist:
        city = None
    return city


@cache_wrap(city_info_cache, pre_fix='province_name', expire=CITY_CACHE_TIME, key_func=lambda *args, **kwargs: ':'.join([str(arg.encode('utf-8') if isinstance(arg, unicode) else arg) for arg in args]))
def _get_capital_by_province(province_name):
    """
    根据城市名返回City对象
    :param : province_name: 身份名
    :return: City对象
    """

    if not province_name:
        return None

    capital_name = _get_capital_name_by_province_name(province_name)
    return _get_city_by_name(capital_name)


def _get_capital_name_by_province_name(province_name):
    """根据省份获取省会城市"""
    m = gm_qq_province_map.get(province_name)
    if not m:
        return

    return m["capital_name"]


def get_city_by_name(city_name):
    return _get_city_by_name(city_name)


@cache_wrap(city_info_cache, pre_fix='city_id', expire=CITY_CACHE_TIME)
def _get_city_by_id(city_id):
    city = None
    try:
        city = City.objects.get(id=city_id)
    except City.DoesNotExist:
        city = None
    return city


def get_city_by_id(city_id):
    return _get_city_by_id(city_id)


def _get_location_tag_id_by_city_id(city_id):
    """return country tag id, provicne tag id and city tag id."""
    if not city_id:
        return None, None, None

    if city_id == CONST_STRINGS.WORLDWIDE:
        return None, None, None

    city_id = str(city_id)
    city_id = city_id.strip()

    if city_id == CONST_STRINGS.NATIONWIDE:
        return settings.CHINA_TAG_ID, None, None

    c = _get_city_by_id(city_id)
    if c:
        try:
            city_tagid = c.tag_id
            p = Province.objects.get(id=c.province_id)
            country_tagid = p.country.tag_id
            return country_tagid, p.tag_id, city_tagid
        except:
            pass

    return None, None, None


def get_geo_related_tags(city_id):
    """return country tag_id, province tag_id, city tag_id."""
    return _get_location_tag_id_by_city_id(city_id)


def get_location_tag_id_by_city_id(city_id):
    """return country tag id and city tag id."""
    # 用的较多，是否应该把城市对应的标签写入缓存？
    c, p, ci = _get_location_tag_id_by_city_id(city_id)
    return c, ci


def calc_distances_by_lat_lnt(lat1, lon1, lat2, lon2):
    lon1, lat1, lon2, lat2 = map(radians, [float(lon1), float(lat1),
                                           float(lon2), float(lat2)])
    if 0 == lon1*lat1*lon2*lat2:
        return ''

    dlon = lon2 - lon1
    dlat = lat2 - lat1

    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
    c = 2 * asin(sqrt(a))
    r = 6371
    return c * r * 1000


def display_distances(d):
    if d == '':
        return ''
    if d <= 100:
        return u'附近'
    elif 100 < d <= 50*1000:
        return str(round(d/1000.0, 1)) + u'km'
    else:
        return u'>50km'


##################################################################
def get_city_by_options(city_id=None, city_name=None, lat=None, lng=None):
    '''
    查询单个城市信息(city_id->city_name->lat,lng)

    param city_id:  城市id    -- beijing
    param city_name: 城市名称  -- 北京
    param lat, lng: 纬度经度
    '''
    if not any([city_id, city_name, lat is not None and lng is not None]):
        return None
    if city_id:
        return _get_city_by_id(city_id)
    city_name = city_name or None
    if not city_name:
        try:
            float(lat), float(lng)
        except ValueError:
            pass
        else:
            city_name = _get_city_name_by_pos(lat, lng)
    if city_name:
        return _get_city_by_name(city_name)

    return None


def get_city_name_by_options(city_id=None, city_name=None, lat=None, lng=None, downgrad=False):
    '''
    查询单个城市信息(city_id->city_name->lat,lng)

    param city_id:  城市id    -- beijing
    param city_name: 城市名称  -- 北京
    param lat, lng: 纬度经度
    param downgrad: 是否启用降级策略。经纬度没有查询到城市的时候我们使用查询到的省份省会城市
    '''
    if not any([city_id, city_name, lat is not None and lng is not None]):
        return None

    if city_id:
        return _get_city_by_id(city_id)

    if city_name:
        return _get_city_by_name(city_name)

    try:
        float(lat), float(lng)
    except ValueError:
        return None

    if downgrad:
        city_info = _get_city_info_by_pos(lat, lng)
        city_name = city_info.get("city_name")
        c = _get_city_by_name(city_name)
        if not c:
            province = city_info.get("province")
            area_info = city_info
            area_info.update({"lat": lat, "lng": lng, "city_name": city_name, "province": province})
            locate_city_logger.info(json.dumps(area_info))
            return _get_capital_by_province(province)
        return c
    else:
        city_name = _get_city_name_by_pos(lat, lng)
        return _get_city_by_name(city_name)

    return None


def set_city_for_user(user_extra, **options):
    city = get_city_by_options(**options)
    if city:
        try:
            user_extra.city = city
            user_extra.save()
            return True
        except:
            return False
    return False

