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

import os
import re
import subprocess
import json
import shutil
import time
import tempfile
from pathlib import Path

import click
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader

import parboil.fields as fields
from .ext import JinjaTimeExtension, jinja_filter_fileify, jinja_filter_slugify


PRJ_FILE  = 'project.json'
META_FILE = '.parboil'


class Project(object):

	def __init__(self, name, repository):
		self._name = name
		if type(repository) is Repository:
			self._repo = repository
			self._root_dir = (repository.root / self._name).resolve()
		else:
			self._repo = None
			self._root_dir = (Path(repository) / self._name).resolve()

	@property
	def name(self):
		return self._name

	@property
	def root(self):
		return self._root_dir

	def exists(self):
		return self._root_dir.is_dir()

	def is_project(self):
		return self.exists() and (self._root_dir / PRJ_FILE).is_file()

	def setup(self, load_project=False):
		# setup config files and paths
		self.project_file = self._root_dir / PRJ_FILE
		self.meta_file = self._root_dir / META_FILE
		self.templates_dir = self._root_dir / 'template'
		self.includes_dir = self._root_dir / 'includes'


		self.meta = dict()
		self.files = dict()
		self.fields = dict()
		self.variables = dict()
		self.templates = list()
		self.includes = list()


		self.templates = list()
		for root, dirs, files in os.walk(self.templates_dir):
			root = Path(root).resolve()
			for name in files:
				dirname = root.relative_to(self.templates_dir)
				self.templates.append(dirname / name)

		self.includes = list()
		for root, dirs, files in os.walk(self.includes_dir):
			root = Path(root).resolve()
			for name in files:
				dirname = root.relative_to(self.includes_dir)
				self.includes.append(dirname / name)

		if load_project:
			self.load()

	def load(self):
		"""Loads the project file and some metadata"""

		if not self.project_file:
			self.setup()

		## project file
		config = dict()
		if self.project_file.exists():
			with open(self.project_file) as f:
				config = json.load(f)
				if 'files' not in config:
					self.files = dict()
				else:
					self.files = config['files']
		else:
			raise ProjectFileNotFoundError('Project file not found.')

		if 'fields' in config:
			for k,v in config['fields'].items():
				if type(v) is not dict:
					self.fields[k] = dict(
						type='default', default=v)
				else:
					self.fields[k] = v

		## metafile
		if self.meta_file.is_file():
			with open(self.meta_file) as f:
				self.meta = {**self.meta, **json.load(f)}

	def fill(self, prefilled=dict()):
		# Read user input (if necessary)
		for key, descr in self.fields.items():
			value = None
			if key in prefilled:
				value = prefilled[key]

			if type(descr) is dict and 'type' in descr:
				if descr['type'] == 'project':
					subproject = Project(descr['name'], self.root.parent)
					subproject.setup(load_project=True)
					subproject.fill({**prefilled, **self.variables})
					self.templates.append(subproject)
				else:
					field_callable = f'field_{descr["type"]}'
					del descr["type"]

					if hasattr(fields, field_callable):
						self.variables[key] = getattr(fields, field_callable)(key=key, **descr, value=value, project=self)

	def compile(self, target_dir, jinja=None):
		if not jinja:
			jinja = Environment(
				loader=ChoiceLoader([
					FileSystemLoader(self.templates_dir),
					PrefixLoader(
						{'includes': FileSystemLoader(self.includes_dir)},
						delimiter=':'
					)
				]),
				extensions=[JinjaTimeExtension]
			)
			jinja.filters['fileify'] = jinja_filter_fileify
			jinja.filters['slugify'] = jinja_filter_slugify

		target_dir = Path(target_dir).resolve()

		result = (list(), list())
		for file in self.templates:
			if type(file) is Project:
				for result in file.compile(target_dir):
					yield result
			else:
				if type(file) is str:
					file_in  = Path(file[9:]) if file.startswith('includes:') else Path(file)
				else:
					file_in = file
				file_out = Path(file_in)
				if str(file_in) in self.files:
					if type(self.files[str(file_in)]) is str:
						file_out = self.files[str(file_in)]

				rel_path = file_in.parent
				abs_path = target_dir / rel_path

				# Set some dynamic values
				boil_vars = dict(
					TPLNAME = self._name,
					RELDIR  = '' if rel_path.name == '' else str(rel_path),
					ABSDIR  = str(abs_path),
					OUTDIR  = str(target_dir)
				)

				path_render = jinja.from_string(str(file_out)).render(**self.variables, BOIL=boil_vars)

				boil_vars['FILENAME'] = Path(path_render).name
				boil_vars['FILEPATH'] = path_render

				# Render template
				tpl_render = jinja.get_template(str(file)).render(**self.variables, BOIL=boil_vars)

				path_render_abs = target_dir / path_render
				if len(tpl_render) > 0:
					if not path_render_abs.parent.exists():
						path_render_abs.parent.mkdir(parents=True)

					with open(path_render_abs, 'w') as f:
						f.write(tpl_render)

					yield (True, str(file), path_render)
				else:
					yield (False, str(file), '')

	def save(self):
		"""Saves the current meta file to disk"""
		if self.meta_file:
			with open(self.meta_file, 'w') as f:
				json.dump(self.meta, f)

	def update(self, hard=False):
		"""Update the template from its original source"""
		if not self.meta_file.exists():
			raise ProjectFileNotFoundError('Template metafile does not exist. Can\'t read update information.')

		if self.meta['source_type'] == 'github':
			git = subprocess.Popen(['git', 'pull', '--rebase'], cwd=self._root_dir)
			git.wait(30)
		elif self.meta['source_type'] == 'local':
			if Path(self.meta['source']).is_dir():
				shutil.rmtree(self._root_dir)
				shutil.copytree(self.meta['source'], self._root_dir)
			else:
				raise ProjectError('Original source directory no longer exists.')
		else:
			raise ProjectError('No source information found.')

		# Update meta file for later updates
		self.meta['updated'] = time.time()
		self.save()

		self.setup(load_project=True)


class Repository(object):

	def __init__(self, root):
		self._root = Path(root)
		self.load()

	@property
	def root(self):
		return self._root

	def exists(self):
		return self._root.is_dir()

	def load(self):
		self._projects = list()
		for child in self._root.iterdir():
			if child.is_dir():
				project_file = child / PRJ_FILE
				if project_file.is_file():
					self._projects.append(child.name)

	def __len__(self):
		return len(self._projects)

	def __iter__(self):
		yield from self._projects

	def is_installed(self, template):
		tpl_dir = self._root / template
		return tpl_dir.is_dir()
		#return Project(template, self).exists()

	def projects(self):
		for prj in self._projects:
			yield self.get_project(prj)

	def get_project(self, template):
		return Project(template, self)

	def install_from_directory(self, template, source, hard=False, is_repo=False):
		"""IF source contains a valid project template it is installed
		into this local repository and the Project object is returned.
		"""
		if self.is_installed(template):
			if not hard:
				raise ProjectExistsError('The template already exists. Delete first or retry install with hard=True.')
			else:
				self._delete(template)

		# check source directory
		source = Path(source).resolve()
		if not source.is_dir():
			raise ProjectFileNotFoundError('Source does not exist.')

		if not is_repo:
			project_file = source / PRJ_FILE
			template_dir = source / 'template'

			if not project_file.is_file():
				raise ProjectFileNotFoundError(f'The source does not contain a {PRJ_FILE} file.')

			if not template_dir.is_dir():
				raise ProjectFileNotFoundError('The source does not contain a template directory.')

			project = self.get_project(template)

			# copy files
			shutil.copytree(source, project.root)

			# create meta file
			project.setup()
			project.meta = {'created': time.time(), 'source_type':'local', 'source': str(source)}
			project.save()

			self.load()
			return project
		else:
			projects = list()

			for child in source.iterdir():
				if child.is_dir():
					project_file = child / PRJ_FILE
					if project_file.is_file():
						try:
							project = self.install_from_directory(child.name, child, hard=hard)
							projects.append(project)
						except:
							pass

			self.load()
			return projects

	def install_from_github(self, template, url, hard=False, is_repo=False):
		if not is_repo:
			# check target dir
			if self.is_installed(template):
				if not hard:
					raise ProjectExistsError('The template already exists. Delete first or retry install with hard=True.')
				else:
					self._delete(template)

			project = self.get_project(template)

			# do git clone
			# TODO: Does this work on windows?
			git = subprocess.Popen(['git', 'clone', url, str(project.root)])
			git.wait(30)

			# create meta file
			project.setup()
			project.meta = {'created': time.time(), 'source_type':'github', 'source': url}
			project.save()

			self.load()
			return project
		else:
			projects = list() # return list of installed projects

			# do git clone into temp folder
			with tempfile.TemporaryDirectory() as temp_repo:
				git = subprocess.Popen(['git', 'clone', url, temp_repo])
				git.wait(30)

				for child in Path(temp_repo).iterdir():
					if child.is_dir():
						project_file = child / PRJ_FILE
						if project_file.is_file():
							try:
								project = self.install_from_directory(child.name, child, hard=hard)
								# remove source data
								del project.meta['source_type']
								del project.meta['source']
								project.save()

								projects.append(project)
							except:
								pass

			self.load()
			return projects

	def uninstall(self, template):
		self._delete(template)

	def _delete(self, template):
		"""Delete a project template from this repository."""
		tpl_dir = self._root / template
		if tpl_dir.is_dir():
			if tpl_dir.is_symlink():
				tpl_dir.unlink()
			else:
				shutil.rmtree(tpl_dir)

	def _reload(self):
		diff = list()
		for child in self._root.iterdir():
			if child.is_dir():
				project_file = child / PRJ_FILE
				if project_file.is_file():
					if not child.name in self._projects:
						diff.append(child.name)
						self._projects.append(child.name)
		self._projects.sort()
		return diff

class ProjectError(Exception):
	pass

class ProjectFileNotFoundError(FileNotFoundError):
	pass

class ProjectExistsError(FileExistsError):
	pass
