# Copyright (C) 2014-2015 Oleh Prypin <blaxpirit@gmail.com>
# 
# This file is part of SixCells.
# 
# SixCells is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# SixCells is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with SixCells.  If not, see <http://www.gnu.org/licenses/>.


import sys as _sys
import os.path as _path
import math as _math
import collections as _collections


def minmax(*args, **kwargs):
    return min(*args, **kwargs), max(*args, **kwargs)


def all_grouped(items, key):
    """Are all the items in one group or not?
    `key` should be a function that says whether 2 items are connected."""
    try:
        grouped = {next(iter(items))}
    except StopIteration:
        return True
    anything_to_add = True
    while anything_to_add:
        anything_to_add = False
        for a in items - grouped:
            if any(key(a, b) for b in grouped):
                anything_to_add = True
                grouped.add(a)
    return len(grouped) == len(items)


def distance(a, b, squared=False):
    "Distance between two items"
    try:
        ax, ay = a
    except TypeError:
        ax, ay = a.x(), a.y()
    try:
        bx, by = b
    except TypeError:
        bx, by = b.x(), b.y()
    r = (ax-bx)**2 + (ay-by)**2
    if not squared:
        r = _math.sqrt(r)
    return r

def angle(a, b=None):
    """Angle between two items: 0 if b is above a, tau/4 if b is to the right of a...
    If b is not supplied, this becomes the angle between (0, 0) and a."""
    try:
        ax, ay = a
    except TypeError:
        ax, ay = a.x(), a.y()
    if b is None:
        return _math.atan2(ax, -ay)
    try:
        bx, by = b
    except TypeError:
        bx, by = b.x(), b.y()
    return _math.atan2(bx-ax, ay-by)


Point = _collections.namedtuple('Point', 'x, y')


class Entity(object):
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return self.name



try:
    _script_name = __FILE__
except NameError:
    _script_name = _sys.argv[0]
_script_path = _path.dirname(_path.realpath(_path.abspath(_script_name)))

def here(*args):
    return _path.join(_script_path, *args)



class cached_property(object):
    "Attribute that is calculated and stored upon first access."
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget
        self.attr = fget.__name__
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        value = self.fget(obj)
        obj.__dict__[self.attr] = value
        return value


class setter_property(object):
    "Attribute that is based only on a setter function; the getter just returns the value"
    def __init__(self, fset):
        self.__doc__ = fset.__doc__
        self.fset = fset
        self.attr = '_' + fset.__name__
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.attr)
    
    def __set__(self, obj, value):
        it = self.fset(obj, value)
        try:
            it = iter(it)
        except TypeError: pass
        else:
            for value in it:
                setattr(obj, self.attr, value)

class event_property(setter_property):
    """An ordinary attribute that can you can get and set,
    but a function without arguments is called when setting it."""
    def __set__(self, obj, value):
        setattr(obj, self.attr, value)
        self.fset(obj)





# Python 2.7 + 3.x compatibility

try:
    unicode
except NameError:
    unicode = str

try:
    basestring
except NameError:
    basestring = (str, bytes)

if isinstance(round(0), float):
    _round = round
    def round(number, ndigits=None):
        if ndigits is None:
            return int(_round(number))
        else:
            return _round(number, ndigits)

def exec_(expression, globals=None, locals=None):
    eval(compile(expression, '<string>', 'exec'), globals, locals)