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

# Copyright 2014-2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
from functools import wraps

from itertools import tee
from functools import total_ordering
from contextlib import contextmanager
import re
import sys

try:
    import json as json_module
except ImportError:
    json_module = None


class SavableIterator:
    """
    Итератор с возможность сохранять и восстанавливать состояние
    """

    def __init__(self, seq):
        self.seq = iter(seq)
        self.back = None

    def __iter__(self):
        return self

    def save(self):
        self.seq, self.back = tee(self.seq)
        return self

    def restore(self):
        if self.back:
            self.seq, self.back = tee(self.back)
        return self

    def next(self):
        return self.seq.next()


@contextmanager
def ignore(exception):
    try:
        yield
    except exception:
        pass


class AddonError(Exception):
    """
    Исключение с добавочным сообщением
    """

    def __init__(self, msg, addon=None):
        self.message = msg
        self.addon = addon
        Exception.__init__(self, msg)


class FalseObject(object):
    """
    Объект-заглушка при любом сравнении возвращает False
    """

    def __lt__(self, other):
        return False

    def __nonzero__(self):
        return False

    __gt__ = __ge__ = __le__ = __eq__ = __ne__ = __lt__


class Sizes(object):
    K = KiB = kibibyte = 1024
    kB = kilobyte = 1000
    M = MiB = mibibyte = K * 1024
    Mb = megabyte = kB * 1000
    G = GiB = gibibyte = M * 1024
    Gb = gigabyte = Mb * 1000
    T = TiB = tibibyte = G * 1024
    Tb = terabyte = Gb * 1000

    def __getattr__(self, name):
        if name.startswith('from_'):
            return lambda x: x * getattr(Sizes, name[5:])
        elif name.startswith('to_'):
            return lambda x: x / getattr(Sizes, name[3:])
        else:
            raise AttributeError


def imap_regexp(re_compiled, l, whole=False):
    """
    Обработать список регулярным выражением и вернуть полученные группы
    """
    if whole:
        retfunc = lambda x: x.group()
    else:
        retfunc = lambda x: x.groups()
    return (retfunc(x) for x in (re_compiled.search(x) for x in l) if x)


def cached(each_instance=False):
    """
    Кэширование результата
    """

    def cache_wrapper(func):
        value = {}

        def wrapper(*args, **kwargs):
            if each_instance:
                if args[0] not in value:
                    value[args[0]] = func(*args, **kwargs)
                return value[args[0]]
            else:
                if None not in value:
                    value[None] = func(*args, **kwargs)
                return value[None]

        return wrapper

    if each_instance in (True, False):
        return cache_wrapper
    else:
        return cache_wrapper(each_instance)


class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kw):
        if cls not in cls._instances:
            create_obj = getattr(super(Singleton, cls), '__call__')
            cls._instances[cls] = create_obj(*args, **kw)
        return cls._instances[cls]


class SingletonParam(type):
    def __init__(cls, name, bases, dicts):
        super(SingletonParam, cls).__init__(name, bases, dicts)
        cls.instance = {}

    def __call__(cls, *args, **kw):
        if hasattr(cls, "param_id"):
            keyarg = cls.param_id(*args, **kw)
        else:
            keyarg = args[0] if args else ""
        if keyarg not in cls.instance:
            create_obj = getattr(super(SingletonParam, cls), '__call__')
            cls.instance[keyarg] = create_obj(*args, **kw)
        return cls.instance[keyarg]


def classificate(iterator):
    """
    Классифицировать элементы по признаку (first первый last последний)
    """

    class Mark:
        def __init__(self, first=False, last=False):
            self.first = first
            self.last = last

        def __repr__(self):
            return "Mark(first=%s,last=%s)" % (self.first, self.last)

    iterator = iter(iterator)
    obj = iterator.next()
    try:
        obj_next = iterator.next()
        yield Mark(first=True, last=False), obj
        try:
            while True:
                obj = obj_next
                obj_next = iterator.next()
                yield Mark(), obj
        except StopIteration:
            yield Mark(first=False, last=True), obj
    except StopIteration:
        yield Mark(first=True, last=True), obj


def debug(prefix="DEBUG {name}({args},{kw})={ret}"):
    def debug_decor(f):
        @wraps(f)
        def wrapper(*args, **kw):
            ret = f(*args, **kw)
            print prefix.format(name=f.func_name, args=args, kw=kw, ret=ret)
            return ret

        return wrapper

    return debug_decor


class ClassPropertyDescriptor(object):
    def __init__(self, fget, fset=None):
        self.fget = fget
        self.fset = fset

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        return self.fget.__get__(obj, klass)()

    def __set__(self, obj, value):
        if not self.fset:
            raise AttributeError("can't set attribute")
        type_ = type(obj)
        return self.fset.__get__(obj, type_)(value)

    def setter(self, func):
        if not isinstance(func, (classmethod, staticmethod)):
            func = classmethod(func)
        self.fset = func
        return self


def classproperty(func):
    if not isinstance(func, (classmethod, staticmethod)):
        func = classmethod(func)

    return ClassPropertyDescriptor(func)


import os
import fcntl
from os import path
import time


class LockError(Exception):
    pass


class Locker(object):
    locker_directory = '/run/calculate/locker'

    def __init__(self, name=None, fn=None, timeout=None):
        if name is not None:
            if not path.exists(self.locker_directory):
                os.makedirs(self.locker_directory)
            self.fn = path.join(self.locker_directory, name)
        else:
            dn = path.dirname(fn)
            if not path.exists(dn):
                os.makedirs(dn)
            self.fn = fn
        self.lockf = None
        if timeout is None:
            self.timeout = 10
        else:
            self.timeout = 0

    def lockfile(self, fn, timeout=None):
        """
        Блокировка файла с таймаутом
        """
        if timeout is None:
            timeout = self.timeout
        for i in range(0, timeout * 40 or 1):
            try:
                self.lockf = os.open(fn, os.O_CREAT | os.O_RDWR)
                fcntl.flock(self.lockf, fcntl.LOCK_EX | fcntl.LOCK_NB)
                return True
            except IOError, e:
                if e.errno != os.errno.EAGAIN:
                    raise e
                os.close(self.lockf)
                self.lockf = None
            except OSError, e:
                if e.errno != os.errno.EEXIST:
                    raise e
            time.sleep(0.25)
        else:
            raise LockError("Lock timeout of %s" % fn)

    def acquire(self):
        if self.lockf is None:
            self.lockfile(self.fn)
        return True

    def release(self):
        if self.lockf is not None:
            os.close(self.lockf)
            self.lockf = None
        return True

    def remove(self):
        self.release()
        if path.exists(self.fn):
            try:
                os.remove(self.fn)
            except (OSError, IOError):
                pass

    def __enter__(self):
        """ Activated when used in the with statement.
            Should automatically acquire a lock to be used in the with block.
        """
        self.acquire()
        return self

    def __exit__(self, type, value, traceback):
        """ Activated at the end of the with statement.
            It automatically releases the lock if it isn't locked.
        """
        self.release()

    def __del__(self):
        """ Make sure that the FileLock instance doesn't leave a lockfile
            lying around.
        """
        self.release()


class Signal(object):
    def __init__(self):
        self.subscribers = []

    def connect(self, subscriber, unique=True):
        if not unique or subscriber not in self.subscribers:
            self.subscribers.append(subscriber)

    def emit(self, *args, **kwargs):
        for subscriber in self.subscribers:
            subscriber(*args, **kwargs)

    def disconnect(self, subscriber, removeall=False):
        while True:
            if subscriber in self.subscribers:
                self.subscribers.remove(subscriber)
                if removeall:
                    continue
            break


def max_default(iterable, key=lambda x: x, default=None):
    try:
        return max(iterable, key=key)
    except ValueError:
        return default


def traverse(o, tree_types=(list, tuple)):
    """
    Вернуть последовательно все элемены списка, "распаковов" встроенные

    [1, 2, [1,2,[4,6,7]], 7] -> (1,2,1,2,4,6,7,7)
    :param o:
    :param tree_types: типы, которые будут распакованы
    :return:
    """
    if isinstance(o, tree_types):
        for value in o:
            for subvalue in traverse(value, tree_types):
                yield subvalue
    else:
        yield o


@total_ordering
class ReverseKey(object):
    """
    Объект для создания ключей обратной сортировки

    s = [("A","B"), ("A","C"), ("B","A")]
    sorted(s, key=lambda x: (x[0], ReverseKey(x[1]))
    """
    __slots__ = ['data']

    def __init__(self, data):
        self.data = data

    def __eq__(self, o):
        return o.data == self.data

    def __lt__(self, o):
        return o.data < self.data

def unique(iterable):
    """
    Исключить из списка дублирующие элементы, не меняя порядка
    """
    seen = {}
    for x in iterable:
        if x in seen:
            continue
        seen[x] = 1
        yield x

def repeater(*args):
    """
    Циклы для повторяющихся действий с ожидаением
    :param args:
    :return:
    """
    yield 0
    for i, x in enumerate(args):
        if x:
            time.sleep(x)
        yield i+1

def iterate_list(l):
    """
    Функция позволяет обрабатывать поэлементно списки, кортежи, исключая
    последовательности, которые ими не являются

    l1 = 1
    l2 = [1,2]
    for x in iterate_list(l1):
        print x

    :param l: Список, кортеж, или прочее
    :return:
    """
    if isinstance(l, (list, tuple)):
        for x in l:
            yield x
    else:
        yield l

def has_any(a, b):
    """
    Содержат ли два списка хотя бы один одинаковый элемент
    :param a:
    :param b:
    :return:
    """
    return any(x in a for x in b)


def unpack_single_opts(args):
    """
    Преобразовать одиночные аргументы
    """
    re_single = re.compile("^-[A-Za-z0-9]")
    re_right = re.compile("^-[A-Za-z0-9]+$")
    for arg in args:
        if re_single.search(arg):
            if not re_right.match(arg):
                sys.stderr.write("Wrong argument %s" % arg)
            for letter in arg[1:]:
                yield "-%s" % letter
        else:
            yield arg

drop_slash = lambda x: x[1:] if x.startswith('/') else x
