# -*- coding: utf-8 -*-
import threading
from collections import defaultdict

import six
from django.db import DEFAULT_DB_ALIAS
from django.db.backends.utils import CursorWrapper
from django.db.transaction import Atomic, get_connection
# Hack for Django < 1.9
try:
    from django.db.transaction import on_commit
except ImportError:
    on_commit = None
from funcy import once, wraps, decorator

from .utils import monkey_mix


__all__ = ('queue_when_in_transaction', 'install_cacheops_transaction_support',
           'transaction_state')


class TransactionState(object):
    def __init__(self):
        self._stack = []

    def begin(self):
        self._stack.append({'cbs': [], 'dirty': False})

    def commit(self):
        context = self._stack.pop()
        if self._stack:
            # savepoint
            self._stack[-1]['cbs'].extend(context['cbs'])
            self._stack[-1]['dirty'] = self._stack[-1]['dirty'] or context['dirty']
        else:
            # transaction
            for func, args, kwargs in context['cbs']:
                func(*args, **kwargs)

    def rollback(self):
        self._stack.pop()

    def append(self, item):
        self._stack[-1]['cbs'].append(item)

    def in_transaction(self):
        return bool(self._stack)

    def mark_dirty(self):
        self._stack[-1]['dirty'] = True

    def is_dirty(self):
        return any(context['dirty'] for context in self._stack)

class TransactionStates(threading.local):
    def __init__(self):
        super(TransactionStates, self).__init__()
        self._states = defaultdict(TransactionState)

    def __getitem__(self, key):
        return self._states[key or DEFAULT_DB_ALIAS]

    def is_dirty(self, dbs):
        return any(self[db].is_dirty() for db in dbs)

transaction_states = TransactionStates()


@decorator
def queue_when_in_transaction(call):
    if transaction_states[call.using].in_transaction():
        transaction_states[call.using].append((call, (), {}))
    else:
        return call()


class AtomicMixIn(object):
    def __enter__(self):
        entering = not transaction_states[self.using].in_transaction()
        transaction_states[self.using].begin()
        self._no_monkey.__enter__(self)
        if on_commit and entering:
            on_commit(transaction_states[self.using].commit, self.using)

    def __exit__(self, exc_type, exc_value, traceback):
        connection = get_connection(self.using)
        self._no_monkey.__exit__(self, exc_type, exc_value, traceback)
        if not connection.closed_in_transaction and exc_type is None and \
                not connection.needs_rollback:
            if not on_commit or transaction_states[self.using].in_transaction():
                transaction_states[self.using].commit()
        else:
            transaction_states[self.using].rollback()


class CursorWrapperMixin(object):
    def callproc(self, procname, params=None):
        result = self._no_monkey.callproc(self, procname, params)
        if transaction_states[self.db.alias].in_transaction():
            transaction_states[self.db.alias].mark_dirty()
        return result

    def execute(self, sql, params=None):
        result = self._no_monkey.execute(self, sql, params)
        if transaction_states[self.db.alias].in_transaction() and is_sql_dirty(sql):
            transaction_states[self.db.alias].mark_dirty()
        return result

    def executemany(self, sql, param_list):
        result = self._no_monkey.executemany(self, sql, param_list)
        if transaction_states[self.db.alias].in_transaction() and is_sql_dirty(sql):
            transaction_states[self.db.alias].mark_dirty()
        return result


def is_sql_dirty(sql):
    # This should not happen as using bytes in Python 3 is against db protocol,
    # but some people will pass it anyway
    if six.PY3 and isinstance(sql, six.binary_type):
        sql = sql.decode()
    # NOTE: not using regex here for speed
    sql = sql.lower()
    chars = 'abcdefghijklmnoprqstuvwxyz_'
    for action in ('update', 'insert', 'delete'):
        p = sql.find(action)
        if p == -1:
            continue
        start, end = p - 1, p + len(action)
        if (start < 0 or sql[start] not in chars) and (end >= len(sql) or sql[end] not in chars):
            return True
    else:
        return False


@once
def install_cacheops_transaction_support():
    monkey_mix(Atomic, AtomicMixIn)
    monkey_mix(CursorWrapper, CursorWrapperMixin)

@@ -562,66 +562,14 @@
 ate(
-object):%0A    def __init__(self):%0A        self._stack = %5B%5D%0A
+list):
 %0A   
@@ -599,23 +599,16 @@
    self.
-_stack.
 append(%7B
@@ -677,31 +677,24 @@
 text = self.
-_stack.
 pop()%0A      
@@ -702,23 +702,16 @@
  if self
-._stack
 :%0A      
@@ -736,39 +736,32 @@
             self
-._stack
 %5B-1%5D%5B'cbs'%5D.exte
@@ -787,39 +787,32 @@
             self
-._stack
 %5B-1%5D%5B'dirty'%5D = 
@@ -811,31 +811,24 @@
 rty'%5D = self
-._stack
 %5B-1%5D%5B'dirty'
@@ -1023,15 +1023,8 @@
 elf.
-_stack.
 pop(
@@ -1034,22 +1034,20 @@
     def 
-append
+push
 (self, i
@@ -1060,31 +1060,24 @@
         self
-._stack
 %5B-1%5D%5B'cbs'%5D.
@@ -1094,72 +1094,8 @@
 m)%0A%0A
-    def in_transaction(self):%0A        return bool(self._stack)%0A%0A
     
@@ -1128,23 +1128,16 @@
     self
-._stack
 %5B-1%5D%5B'di
@@ -1233,15 +1233,8 @@
 self
-._stack
 )%0A%0Ac
@@ -1700,33 +1700,16 @@
 l.using%5D
-.in_transaction()
 :%0A      
@@ -1741,22 +1741,20 @@
 .using%5D.
-append
+push
 ((call, 
@@ -1901,33 +1901,16 @@
 f.using%5D
-.in_transaction()
 %0A       
@@ -2457,33 +2457,16 @@
 f.using%5D
-.in_transaction()
 :%0A      
@@ -2776,33 +2776,16 @@
 b.alias%5D
-.in_transaction()
 :%0A      
@@ -3005,33 +3005,16 @@
 b.alias%5D
-.in_transaction()
  and is_
@@ -3271,25 +3271,8 @@
 ias%5D
-.in_transaction()
  and

Simplify TransactionState object