#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Generate static site
'''

import os
import yaml
import time
import shutil
import logging
import optparse
from types import SimpleNamespace

from threading import Thread
from collections import defaultdict
from jinja2 import Environment, FileSystemLoader, pass_context


# Core config params
CONFIG_FILE = 'config.yaml'
CONTENT_DIR = 'content'
LAYOUT_DIR = 'layout'
STATIC_DIR = 'static'
BUILD_DIR = 'public'


# Default item params

SITE_PAGES = 'pages'

PAGE_URL = 'url'
PAGE_NAME = 'name'
PAGE_PATH = 'path'
PAGE_LAYOUT = 'layout'
PAGE_CONTENT = 'content'

log = logging.getLogger('tagy')
log.setLevel(logging.INFO)
log.addHandler(logging.StreamHandler())

def generate(silent=False):
	log.info('Generate site')
	site = load_site()
	generate_site(site, silent)

def load_site():
	# load basic configs
	site = load_config()

	# add pages
	site[SITE_PAGES] = load_content()

	# build indexes
	for page in site.pages:
		# check all indexes
		for indexname in iter(site.indexes):
			index = site.indexes[indexname]
			terms = getattr(page, indexname, None)
			if isinstance(terms, list):
				for term in terms:
					index.setdefault('terms', {}).setdefault(term, []).append(page)

	return site

def load_config(path=CONFIG_FILE):
	with open(path, 'r') as f:
		return Config(yaml.safe_load(f))

def load_content(dir=CONTENT_DIR):
	pages = []
	for subdir, dirs, files in os.walk(dir):
		for file in files:
			if file.startswith('.'):
				continue
			path = os.path.join(subdir, file)
			page = load_page(path)
			pages.append(page)
	return pages
	
CONFIG = '---\n'

import mistune
def load_page(path):
	with open(path, 'r') as f:
		try:
			content = f.read()
			i = content.find(CONFIG)
			page = yaml.safe_load(content[:i]) if i > 0 else {}
			start = i+len(CONFIG) if i > 0 else 0
			content = content[start:]
			if path.endswith('.html') | path.endswith('.md'):
				content = mistune.markdown(content)
			page[PAGE_CONTENT] = content
			path = path[ : path.index('.')]
			page[PAGE_NAME] = os.path.basename(path)
			if path.endswith('/index'): # cut index page
				path = path[ : path.index('/index')]
			page[PAGE_PATH] = path[len(CONTENT_DIR + '/') : ]
			return Config(page)
		except Exception as e:
			log.warning('Failed to read page "%s"', path, e)

# Generate logic

env = Environment(loader=FileSystemLoader(LAYOUT_DIR))
_pagination_queue = []

def _get_env():
	if env.loader.searchpath != [LAYOUT_DIR]:
		env.loader = FileSystemLoader(LAYOUT_DIR)
	return env

def generate_site(site, silent):
	clear()
	_pagination_queue.clear()

	for page in site.pages:
		try:
			generate_page(page, site)
		except Exception as e:
			if silent:
				log.warning('Failed to generate page "%s"', page[PAGE_PATH])
			else:
				raise e

	i = 0
	while i < len(_pagination_queue):
		page = _pagination_queue[i]
		try:
			generate_page(page, site)
		except Exception as e:
			if silent:
				log.warning('Failed to generate pagination page "%s"', page[PAGE_PATH])
			else:
				raise e
		i += 1

	for name in iter(site.indexes):
		generate_index(name, site)

def clear():
	# Clear dir
	folder = BUILD_DIR
	if os.path.exists(folder):
		for name in os.listdir(folder):
			path = os.path.join(folder, name)
			try:
				if os.path.isfile(path):
					os.unlink(path)
				else: 
					shutil.rmtree(path)
			except Exception as e:
				log.exception(e)
	else:
		os.makedirs(folder)

	# Copy static dir
	for file_name in os.listdir(STATIC_DIR):
		full_file_name = os.path.join(STATIC_DIR, file_name)
		if (os.path.isfile(full_file_name)):
			shutil.copy(full_file_name, BUILD_DIR)
		else:
			shutil.copytree(full_file_name, os.path.join(BUILD_DIR, file_name))


def generate_page(page, site):
	# render content
	e = _get_env()
	content = e.from_string(page.content)
	page.content = content.render({'page': page, 'site': site})

	# render layout
	template = e.get_template(get_template(page))
	html = template.render({'page': page, 'site': site})

	# generate page
	path = get_build_path(page)
	with open(path, 'w') as f:
		f.write(html)

def generate_index(name, site):
	index = getattr(site.indexes, name)
	for term in iter(index.terms):
		# TODO: remove layout from yaml
		template = _get_env().get_template(get_template(index))
		
		page = Config()
		page.path = index.url + '/' + term

		html = template.render({'term': term, 'pages': index.terms[term], 'site': site})
		with open(get_build_path(page), 'w') as f:
			f.write(html)



def get_template(page):
	# TODO: impl inheritance logic
	if PAGE_LAYOUT in page:
		return page[PAGE_LAYOUT]
	return 'index.html'

# Return fancy url for path
def get_build_path(page):
	if PAGE_URL in page:
		return BUILD_DIR + '/' + page.url

	dir = BUILD_DIR + '/' + page.path

	try:
		os.stat(dir)
	except:
		os.makedirs(dir)
	return dir + '/index.html'

def get_last_update():
	'''Get last update time for content directories'''
	# path = '/tmp/test_tracking/'
	files = []
	subdirs = []

	last = None
	for folder in [CONTENT_DIR, STATIC_DIR, LAYOUT_DIR]:
		for root, dirs, filenames in os.walk(folder):
			for f in filenames:
				filename = os.path.relpath(os.path.join(root, f), folder)
				file_mtime = os.path.getmtime(os.path.join(folder, filename))
				if  last is None or file_mtime > last:
					last = file_mtime 
	return last

def serve(port=1313):
	thread = Thread(target=watch)
	thread.daemon = True
	thread.start()

	# start server
	os.system("cd %s; python3 -m http.server %d" % (BUILD_DIR, port))

def watch():
	'''Watch file changed in infinite loop'''
	updated = None
	while True:
		last = get_last_update()
		if updated != last:
			updated = last
			try:
				generate(silent=True)
			except Exception as e:
				log.exception(e)
		time.sleep(1)


# Jinja filters

def breadcrumbs(path):
	'''Return path chunks'''
	result = []
	if path == '':
		return result
	current = ''
	parts = path.split('/')
	for part in parts:
		if len(current) > 0:
			current = current + '/'
		current = current + part
		result.append(current)
	return result

def where(iterator, param, value=True):
	result = []
	for item in iterator:
		if param in item:
			if isinstance(value, bool): # check item just has a param
				result.append(item)
			elif isinstance(value, str): # for strings check starts with
				if item[param].startswith(value):
					result.append(item)
			elif isinstance(value, int): # for numbers check equality
				if item[param] == value:
					result.append(item)
	return result

from PIL import Image
def get_thumbnail(value, size=(100, 100), dir=BUILD_DIR):
	file_path = dir + value
	file, ext = os.path.splitext(file_path)
	path = (file + '-%dx%d' + ext) % size
	im = Image.open(file_path)
	im.thumbnail(size, Image.LANCZOS)
	im.save(path, "PNG")
	return path[len(dir) : ]

@pass_context
def paginate(ctx, items, per_page=10):
	page = ctx.get('page')
	page_num = 1
	if page and page.pagination:
		page_num = page.pagination.page_num or 1

	items = list(items)
	total = len(items)
	total_pages = max(1, (total + per_page - 1) // per_page)
	start = (page_num - 1) * per_page
	page_items = items[start:start + per_page]

	if page_num == 1 and page and total_pages > 1:
		base_path = page.path
		for n in range(2, total_pages + 1):
			new_page = Config(dict(page))
			new_page.pagination = Config({'page_num': n})
			new_page.path = base_path + '/' + str(n)
			_pagination_queue.append(new_page)

	return SimpleNamespace(
		page_num=page_num,
		items=page_items,
		total=total,
		per_page=per_page,
		total_pages=total_pages,
		has_prev=page_num > 1,
		has_next=page_num < total_pages,
		prev_num=page_num - 1 if page_num > 1 else None,
		next_num=page_num + 1 if page_num < total_pages else None,
		base_path=(page.path.rsplit('/', 1)[0] if page_num > 1 else page.path) if page else '',
	)

env.tests['equalto'] = lambda value, other : value == other
env.filters['where'] = where
env.filters['breadcrumbs'] = breadcrumbs
env.filters['thumb'] = get_thumbnail
env.globals['paginate'] = paginate



# Model for config params
class Config(dict):
	def __getattr__(self, attr):
		if attr not in self:
			return None
		result = self[attr]
		if isinstance(result, dict):
			# TODO: why?
			result = Config(result)
		return result
	def __setattr__(self, attr, value):
		self[attr] = value


# =============================================================================
# options
# =============================================================================

def options():
    """Parse and validate command line arguments."""

    usage = ("Usage: %prog --build [OPTIONS] [path/to/project]\n"
             "       %prog --serve [OPTIONS] [path/to/project]\n"
             "\n"
             "       Project path is optional, '.' is used as default.")

    op = optparse.OptionParser(usage=usage)

    op.add_option("-b" , "--build", action="store_true", default=False,
                  help="build project")
    op.add_option("-s" , "--serve", action="store_true", default=False,
                  help="serve project")

    og = optparse.OptionGroup(op, "Serve options")
    og.add_option("" , "--port", default=1313,
                  metavar="PORT", type="int",
                  help="port for serving (default: 1313)")
    op.add_option_group(og)

    opts, args = op.parse_args()

    if opts.build + opts.serve < 1:
        op.print_help()
        op.exit()

    opts.project = args and args[0] or "."

    return opts

# =============================================================================
# main
# =============================================================================

def main():

    opts = options()

    if opts.build:
        generate()
    if opts.serve:
        serve(opts.port)

if __name__ == '__main__':
    main()
