# -*- coding: utf-8 -*-

import math
import random
import re
import threading

from util import hook


def sanitize(s):
    return re.sub(r'[\x00-\x1f]', '', s)


@hook.command
def munge(inp, munge_count=0):
    reps = 0
    for n in xrange(len(inp)):
        rep = character_replacements.get(inp[n])
        if rep:
            inp = inp[:n] + rep.decode('utf8') + inp[n + 1:]
            reps += 1
            if reps == munge_count:
                break
    return inp


class PaginatingWinnower(object):

    def __init__(self):
        self.lock = threading.Lock()
        self.last_input = []
        self.recent = set()

    def winnow(self, inputs, limit=400, ordered=False):
        "remove random elements from the list until it's short enough"
        with self.lock:
            # try to remove elements that were *not* removed recently
            inputs_sorted = sorted(inputs)
            if inputs_sorted == self.last_input:
                same_input = True
            else:
                same_input = False
                self.last_input = inputs_sorted
                self.recent.clear()

            combiner = lambda l: u', '.join(l)
            suffix = ''

            while len(combiner(inputs)) >= limit:
                if same_input and any(inp in self.recent for inp in inputs):
                    if ordered:
                        for inp in self.recent:
                            if inp in inputs:
                                inputs.remove(inp)
                    else:
                        inputs.remove(
                            random.choice([inp for inp in inputs if inp in self.recent]))
                else:
                    if ordered:
                        inputs.pop()
                    else:
                        inputs.pop(random.randint(0, len(inputs) - 1))
                suffix = ' ...'

            self.recent.update(inputs)
            return combiner(inputs) + suffix

winnow = PaginatingWinnower().winnow


def add_tag(db, chan, nick, subject):
    match = db.execute('select * from tag where lower(nick)=lower(?) and'
                       ' chan=? and lower(subject)=lower(?)',
                       (nick, chan, subject)).fetchall()
    if match:
        return 'already tagged'

    db.execute('replace into tag(chan, subject, nick) values(?,?,?)',
               (chan, subject, nick))
    db.commit()

    return 'tag added'


def delete_tag(db, chan, nick, del_tag):
    count = db.execute('delete from tag where lower(nick)=lower(?) and'
                       ' chan=? and lower(subject)=lower(?)',
                       (nick, chan, del_tag)).rowcount
    db.commit()

    if count:
        return 'deleted'
    else:
        return 'tag not found'


def get_tag_counts_by_chan(db, chan):
    tags = db.execute("select subject, count(*) from tag where chan=?"
                      " group by lower(subject)"
                      " order by lower(subject)", (chan,)).fetchall()

    tags.sort(key=lambda x: x[1], reverse=True)
    if not tags:
        return 'no tags in %s' % chan
    return winnow(['%s (%d)' % row for row in tags], ordered=True)


def get_tags_by_nick(db, chan, nick):
    tags = db.execute("select subject from tag where lower(nick)=lower(?)"
                      " and chan=?"
                      " order by lower(subject)", (nick, chan)).fetchall()
    if tags:
        return 'tags for "%s": ' % munge(nick, 1) + winnow([
            tag[0] for tag in tags])
    else:
        return ''


def get_nicks_by_tagset(db, chan, tagset):
    nicks = None
    for tag in tagset.split('&'):
        tag = tag.strip()

        current_nicks = db.execute("select lower(nick) from tag where " +
                                   "lower(subject)=lower(?)"
                                   " and chan=?", (tag, chan)).fetchall()

        if not current_nicks:
            return "tag '%s' not found" % tag

        if nicks is None:
            nicks = set(current_nicks)
        else:
            nicks.intersection_update(current_nicks)

    nicks = [munge(x[0], 1) for x in sorted(nicks)]
    if not nicks:
        return 'no nicks found with tags "%s"' % tagset
    return 'nicks tagged "%s": ' % tagset + winnow(nicks)


@hook.command
def tag(inp, chan='', db=None):
    '.tag <nick> <tag> -- marks <nick> as <tag> {related: .untag, .tags, .tagged, .is}'

    db.execute('create table if not exists tag(chan, subject, nick)')

    add = re.match(r'(\S+) (.+)', inp)

    if add:
        nick, subject = add.groups()
        if nick.lower() == 'list':
            return 'tag syntax has changed. try .tags or .tagged instead'
        elif nick.lower() == 'del':
            return 'tag syntax has changed. try ".untag %s" instead' % subject
        return add_tag(db, chan, sanitize(nick), sanitize(subject))
    else:
        tags = get_tags_by_nick(db, chan, inp)
        if tags:
            return tags
        else:
            return tag.__doc__


@hook.command
def untag(inp, chan='', db=None):
    '.untag <nick> <tag> -- unmarks <nick> as <tag> {related: .tag, .tags, .tagged, .is}'

    delete = re.match(r'(\S+) (.+)$', inp)

    if delete:
        nick, del_tag = delete.groups()
        return delete_tag(db, chan, nick, del_tag)
    else:
        return untag.__doc__


@hook.command
def tags(inp, chan='', db=None):
    '.tags <nick>/list -- get list of tags for <nick>, or a list of tags {related: .tag, .untag, .tagged, .is}'
    if inp == 'list':
        return get_tag_counts_by_chan(db, chan)

    tags = get_tags_by_nick(db, chan, inp)
    if tags:
        return tags
    else:
        return get_nicks_by_tagset(db, chan, inp)


@hook.command
def tagged(inp, chan='', db=None):
    '.tagged <tag> [& tag...] -- get nicks marked as <tag> (separate multiple tags with &) {related: .tag, .untag, .tags, .is}'

    return get_nicks_by_tagset(db, chan, inp)

@hook.command('is')
def is_tagged(inp, chan='', db=None):
    '.is <nick> <tag> -- checks if <nick> has been marked as <tag> {related: .tag, .untag, .tags, .tagged}'

    args = re.match(r'(\S+) (.+)$', inp)

    if args:
        nick, tag = args.groups()
        found = db.execute("select 1 from tag"
                           " where lower(nick)=lower(?)"
                           "   and lower(subject)=lower(?)"
                           "   and chan=?", (nick, tag, chan)).fetchone()
        if found:
            return 'yes'
        else:
            return 'no'
    else:
        return is_tagged.__doc__

def distance(lat1, lon1, lat2, lon2):
    deg_to_rad = math.pi / 180
    lat1 *= deg_to_rad
    lat2 *= deg_to_rad
    lon1 *= deg_to_rad
    lon2 *= deg_to_rad

    R = 6371  # km
    d = math.acos(math.sin(lat1) * math.sin(lat2) +
                  math.cos(lat1) * math.cos(lat2) *
                  math.cos(lon2 - lon1)) * R
    return d


@hook.command(autohelp=False)
def near(inp, nick='', chan='', db=None):
    try:
        loc = db.execute("select lat, lon from location where chan=? and nick=lower(?)",
                (chan, nick)).fetchone()
    except db.OperationError:
        loc = None

    if loc is None:
        return 'use .weather <loc> first to set your location'

    lat, lon = loc

    db.create_function('distance', 4, distance)
    nearby = db.execute("select nick, distance(lat, lon, ?, ?) as dist from location where chan=?"
                        " and nick != lower(?) order by dist limit 20", (lat, lon, chan, nick)).fetchall()

    in_miles = 'mi' in inp.lower()

    out = '(km) '
    factor = 1.0
    if in_miles:
        out = '(mi) '
        factor = 0.621

    while nearby and len(out) < 200:
        nick, dist = nearby.pop(0)
        out += '%s:%.0f ' % (munge(nick, 1), dist * factor)

    return out


character_replacements = {
    'a': 'ä',
#    'b': 'Б',
    'c': 'ċ',
    'd': 'đ',
    'e': 'ë',
    'f': 'ƒ',
    'g': 'ġ',
    'h': 'ħ',
    'i': 'í',
    'j': 'ĵ',
    'k': 'ķ',
    'l': 'ĺ',
#    'm': 'ṁ',
    'n': 'ñ',
    'o': 'ö',
    'p': 'ρ',
#    'q': 'ʠ',
    'r': 'ŗ',
    's': 'š',
    't': 'ţ',
    'u': 'ü',
#    'v': '',
    'w': 'ω',
    'x': 'χ',
    'y': 'ÿ',
    'z': 'ź',
    'A': 'Å',
    'B': 'Β',
    'C': 'Ç',
    'D': 'Ď',
    'E': 'Ē',
#    'F': 'Ḟ',
    'G': 'Ġ',
    'H': 'Ħ',
    'I': 'Í',
    'J': 'Ĵ',
    'K': 'Ķ',
    'L': 'Ĺ',
    'M': 'Μ',
    'N': 'Ν',
    'O': 'Ö',
    'P': 'Р',
#    'Q': 'Ｑ',
    'R': 'Ŗ',
    'S': 'Š',
    'T': 'Ţ',
    'U': 'Ů',
#    'V': 'Ṿ',
    'W': 'Ŵ',
    'X': 'Χ',
    'Y': 'Ỳ',
    'Z': 'Ż'}
