# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function

import itertools as it, operator as op, functools as ft
from subprocess import Popen, PIPE
from fabric import api, utils
from fabsetup import config
from contextlib import contextmanager
import os, sys, types

from .gitshelve import open as gitshelve


from collections import MutableMapping

class StateDB(MutableMapping):

	_backend = None
	_updates = None

	def __init__(self, repository):
		self.repository = repository
		self.workdir = self.repository.rsplit('.git', 1)[0]\
			if self.repository.rstrip(os.sep).endswith('{}.git'.format(os.sep))\
			else self.repository


	@property
	def backend(self):
		if self._backend is None:
			self._updates = set()
			self._backend = gitshelve(repository=self.repository)
		return self._backend


	@staticmethod
	def _key(key):
		if isinstance(key, tuple): key = os.path.join(*key)
		if not key.startswith('{}{}'.format(api.env.host, os.sep)):
			key = os.path.join(api.env.host, key)
		return key

	def __iter__(self): return iter(self.backend)
	def __len__(self): return len(self.backend)

	def __getitem__(self, key, past=0):
		return self.backend[self._key(key)]
	def __setitem__(self, key, value):
		key = self._key(key)
		self.backend[key] = value
		self._updates.add(key)
	def __delitem__(self, key):
		key = self._key(key)
		del self.backend[key]
		self._updates.add(key)

	def tempfile(self, key='tmp', suffix=''):
		from tempfile import NamedTemporaryFile
		from os.path import dirname, basename, join
		key = self._key(key)
		path = dirname(join(self.workdir, key))
		db_path = ''
		for slug in path.split(os.sep):
			db_path = join(db_path, slug)
			if not os.path.exists(db_path): os.mkdir(db_path)
		return NamedTemporaryFile( dir=path,
			prefix='{}.'.format(basename(key)), suffix=suffix )

	@contextmanager
	def file(self, key):
		key = self._key(key)
		with self.tempfile(key) as tmp:
			tmp.write(self[key])
			tmp.flush(), tmp.seek(0)
			yield tmp


	patch_errors = AssertionError, ValueError

	def patch(self, key, diff, src=None, dry_run=False, reverse=False):
		from collections import deque
		from subprocess import Popen, PIPE

		if isinstance(diff, types.StringTypes):
			from io import BytesIO
			diff = BytesIO(diff)
		elif diff is True:
			diff = self.diff(key=key)
			if not diff: return # identical
			diff = diff.splitlines()

		if src is None:
			src_file = self.tempfile(key)
			src_file.write(self.get(key, ''))
			src_file.flush()
			src = src_file.name
		else: src_file = None

		try:
			buff = deque(maxlen=3)
			res, patched = list(), False
			for line in diff:
				if len(buff) == 3:
					if buff[0].startswith(b'--- ')\
							and buff[1].startswith(b'+++ ')\
							and buff[2].startswith(b'@@ '):
						if patched:
							raise ValueError('Diff contains more than one file header')
						res.append(buff[0])
						res.append('+++ {}\n'.format(os.path.basename(src)))
						res.append(buff[2])
						buff.clear()
						patched = True
					else: res.append(buff[0])
				buff.append(line)
			res.extend(buff)
			if not patched:
				raise ValueError('No file headers found in diff ')

			null = open('/dev/null', 'wb')
			ext = [] if not reverse else ['-R']
			for ext_pre in [['--dry-run'], []] if not dry_run else [['--dry-run']]:
				proc = Popen(
					['patch', '--no-backup-if-mismatch', '-r-', '-tul', '-p0'] + ext + ext_pre,
					cwd=os.path.dirname(src), stdin=PIPE )
				for line in res:
					if line[-1] != b'\n': line = b'{}\n'.format(line)
					proc.stdin.write(line)
				proc.stdin.close()
				if proc.wait():
					raise AssertionError('patch command returned error')

			if not dry_run: self[key] = open(src).read() # patch doesn't do it inplace

		finally:
			if src_file: src_file.close()


	def diff(self, dst=None, key=None, past=0, src=None, visual=False):
		if src is None:
			if key is None:
				raise ValueError('Either key or src should be specified')
			src_file = self.tempfile(key)
			src_file.write(self.get(key, ''))
			src_file.flush()
			src = src_file.name
		else: src_file = None
		if not dst:
			if key is None:
				raise ValueError('Either key or dst should be specified')
			key = self._key(key)
			dst = os.path.join(self.workdir, key)

		try:
			proc = Popen([ config.base.diff_visual
				if visual else 'diff', '-uw', src, dst ], stdout=PIPE)
			diff = proc.stdout.read()
			if proc.wait(): return diff
			else: return False
		finally:
			if src_file: src_file.close()


	def sync(self, message=None, indent=utils.indent):
		if not self._backend: return
		if message is None:
			message = 'State sync. Updates({}):\n{}'\
				.format(len(self._updates), indent('\n'.join(self._updates)))
		self._updates = list()
		self.backend.commit(message)

	__del__ = sync


state = StateDB(config.base.state)
