from datetime import datetime
import hashlib
import logging
import os
import os.path
import shutil

import jinja2

import russell.content
import russell.feed
import russell.sitemap

LOG = logging.getLogger(__name__)


def _listfiles(root_dir):
	results = set()

	for root, _, files in os.walk(root_dir):
		for file in files:
			results.add(os.path.join(root, file))

	return results


def make_link(title, url, blank=False):
	"""
	Make a HTML link out of an URL.

	Args:
	  title (str): Text to show for the link.
	  url (str): URL the link will point to.
	  blank (bool): If True, appends target=_blank, noopener and noreferrer to
	    the <a> element. Defaults to False.
	"""
	attrs = 'href="%s"' % url
	if blank:
		attrs += ' target="_blank" rel="noopener noreferrer"'
	return '<a %s>%s</a>' % (attrs, title)


class BlogEngine:
	"""
	The main instance that contains blog configuration and content, as well as
	generating end results.
	"""
	def __init__(self, root_path, root_url, site_title, site_desc=None):
		"""
		Constructor.

		Args:
		  root_path (str): Full path to the directory which contains the posts,
		    pages, templates etc. directories.
		  root_url (str): The root URL of your website.
		  site_title (str): The title of your website.
		  site_desc (str): A subtitle or description of your website.
		"""
		self.root_path = root_path
		self.root_url = root_url
		self.site_title = site_title
		self.site_desc = site_desc

		self.cm = russell.content.ContentManager(root_url) #pylint: disable=invalid-name
		self.pages = self.cm.pages
		self.posts = self.cm.posts
		self.tags = self.cm.tags

		self.asset_hash = {}

		self.jinja = jinja2.Environment(
			loader=jinja2.FileSystemLoader(os.path.join(root_path, 'templates')),
		)
		self.jinja.globals.update({
			'a': make_link,
			'asset_hash': self.asset_hash,
			'asset_url': self.get_asset_url,
			'now': datetime.now(),
			'root_url': self.root_url,
			'site_description': self.site_desc,
			'site_title': self.site_title,
			'tags': self.tags,
		})

	def get_asset_url(self, path):
		"""
		Get the URL of an asset. If asset hashes are added and one exists for
		the path, it will be appended as a query string.

		Args:
		  path (str): Path to the file, relative to your "assets" directory.
		"""
		url = self.root_url + '/assets/' + path
		if path in self.asset_hash:
			url += '?' + self.asset_hash[path]
		return url

	def add_pages(self, path='pages'):
		"""
		Look through a directory for markdown files and add them as pages.
		"""
		pages_path = os.path.join(self.root_path, path)
		pages = []
		for file in _listfiles(pages_path):
			page_dir = os.path.relpath(os.path.dirname(file), pages_path)
			if page_dir == '.':
				page_dir = None
			pages.append(self.cm.Page.from_file(file, directory=page_dir))
		self.cm.add_pages(pages)

	def add_posts(self, path='posts'):
		"""
		Look through a directory for markdown files and add them as posts.
		"""
		path = os.path.join(self.root_path, path)
		self.cm.add_posts([
			self.cm.Post.from_file(file)
			for file in _listfiles(path)
		])

	def copy_assets(self, path='assets'):
		"""
		Copy assets into the destination directory.
		"""
		path = os.path.join(self.root_path, path)
		for root, _, files in os.walk(path):
			for file in files:
				fullpath = os.path.join(root, file)
				relpath = os.path.relpath(fullpath, path)
				copy_to = os.path.join(self._get_dist_path(relpath, directory='assets'))
				LOG.debug('copying %r to %r', fullpath, copy_to)
				shutil.copyfile(fullpath, copy_to)

	def add_asset_hashes(self, path='dist/assets'):
		"""
		Scan through a directory and add hashes for each file found.
		"""
		for fullpath in _listfiles(os.path.join(self.root_path, path)):
			relpath = fullpath.replace(self.root_path + '/' + path + '/', '')
			md5sum = hashlib.md5(open(fullpath, 'rb').read()).hexdigest()
			LOG.debug('MD5 of %s (%s): %s', fullpath, relpath, md5sum)
			self.asset_hash[relpath] = md5sum

	def get_posts(self, num=None, tag=None, private=False):
		"""
		Get all the posts added to the blog.

		Args:
		  num (int): Optional. If provided, only return N posts (sorted by date,
		    most recent first).
		  tag (Tag): Optional. If provided, only return posts that have a
		    specific tag.
		  private (bool): By default (if False), private posts are not included.
		    If set to True, private posts will also be included.
		"""
		posts = self.posts

		if not private:
			posts = [post for post in posts if post.public]

		if tag:
			posts = [post for post in posts if tag in post.tags]

		if num:
			return posts[:num]
		return posts

	def _get_dist_path(self, path, directory=None):
		if isinstance(path, str):
			path = [path]
		if directory:
			path.insert(0, directory)
		return os.path.join(self.root_path, 'dist', *path)

	def _get_template(self, template):
		if isinstance(template, str):
			template = self.jinja.get_template(template)
		return template

	def generate_pages(self):
		"""
		Generate HTML out of the pages added to the blog.
		"""
		for page in self.pages:
			self.generate_page(page.slug, template='page.html.jinja', page=page)

	def generate_posts(self):
		"""
		Generate single-post HTML files out of posts added to the blog. Will not
		generate front page, archives or tag files - those have to be generated
		separately.
		"""
		for post in self.posts:
			self.generate_page(
				['posts', post.slug],
				template='post.html.jinja',
				post=post,
			)

	def generate_tags(self):
		"""
		Generate one HTML page for each tag, each containing all posts that
		match that tag.
		"""
		for tag in self.tags:
			posts = self.get_posts(tag=tag, private=True)
			self.generate_page(['tags', tag.slug],
				template='archive.html.jinja', posts=posts)

	def generate_page(self, path, template, **kwargs):
		"""
		Generate the HTML for a single page. You usually don't need to call this
		method manually, it is used by a lot of other, more end-user friendly
		methods.

		Args:
		  path (str): Where to place the page relative to the root URL. Usually
		    something like "index", "about-me", "projects/example", etc.
		  template (str): Which jinja template to use to render the page.
		  **kwargs: Kwargs will be passed on to the jinja template. Also, if
		    the `page` kwarg is passed, its directory attribute will be
		    prepended to the path.
		"""
		directory = None
		if kwargs.get('page'):
			directory = kwargs['page'].dir

		path = self._get_dist_path(path, directory=directory)
		if not path.endswith('.html'):
			path = path + '.html'

		if not os.path.isdir(os.path.dirname(path)):
			os.makedirs(os.path.dirname(path))

		html = self._get_template(template).render(**kwargs)

		with open(path, 'w+') as file:
			file.write(html)

	def generate_index(self, num_posts=5):
		"""
		Generate the front page, aka index.html.
		"""
		posts = self.get_posts(num=num_posts)
		self.generate_page('index', template='index.html.jinja', posts=posts)

	def generate_archive(self):
		"""
		Generate the archive HTML page.
		"""
		self.generate_page('archive', template='archive.html.jinja', posts=self.get_posts())

	def generate_rss(self, path='rss.xml', only_excerpt=True, https=False):
		"""
		Generate the RSS feed.

		Args:
		  path (str): Where to save the RSS file. Make sure that your jinja
		    templates refer to the same path using <link>.
		  only_excerpt (bool): If True (the default), don't include the full
		    body of posts in the RSS. Instead, include the first paragraph and
		    a "read more" link to your website.
		  https (bool): If True, links inside the RSS with relative scheme (e.g.
		    //example.com/something) will be set to HTTPS. If False (the
		    default), they will be set to plain HTTP.
		"""
		feed = russell.feed.get_rss_feed(self, only_excerpt=only_excerpt, https=https)
		feed.rss_file(self._get_dist_path(path))

	def generate_sitemap(self, path='sitemap.xml', https=False):
		"""
		Generate an XML sitemap.

		Args:
		  path (str): The name of the file to write to.
		  https (bool): If True, links inside the sitemap with relative scheme
		    (e.g. example.com/something) will be set to HTTPS. If False (the
		    default), they will be set to plain HTTP.
		"""
		sitemap = russell.sitemap.generate_sitemap(self, https=https)
		self.write_file(path, sitemap)

	def write_file(self, path, contents):
		"""
		Write a file of any type to the destination path. Useful for files like
		robots.txt, manifest.json, and so on.

		Args:
		  path (str): The name of the file to write to.
		  contents (str or bytes): The contents to write.
		"""
		path = self._get_dist_path(path)
		if not os.path.isdir(os.path.dirname(path)):
			os.makedirs(os.path.dirname(path))
		if isinstance(contents, bytes):
			mode = 'wb+'
		else:
			mode = 'w'
		with open(path, mode) as file:
			file.write(contents)
