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

import re, math, ConfigParser, time, platform, os
from subprocess import Popen, PIPE
from os import path, listdir, makedirs, system
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urllib import unquote
from datetime import datetime, tzinfo, timedelta
from shutil import copyfile
from distutils.sysconfig import get_python_lib
from jinja2.loaders import FileSystemLoader
from jinja2 import Environment
from werkzeug import script
from markdown import markdown
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler


class Config:
    def __init__(self):
        self.is_parsed = False
        self._parser = ConfigParser.RawConfigParser(allow_no_value=True)
        rc_path = path.expanduser('~/.microrc')
        if path.exists(rc_path):
            self._parser.read(rc_path)
            self.is_parsed = True

    def __getattr__(self, option):
        if self.is_parsed:
            config_list = {
                'month': 'locale',
                'days': 'locale',
                'public': 'storage',
                'templates': 'storage',
                'pages': 'storage',
                'posts': 'storage',
                'to': 'sync',
                'key': 'sync'
            }
            if option in ['templates', 'pages', 'posts', 'public', 'build']:
                return path.expanduser(self._parser.get('storage', option))
            elif option == 'blog_path':
                return path.join(self.build, "blog")
            elif option == 'categories_path':
                return path.join(self.blog_path, "categories")
            elif option == 'archives_path':
                return path.join(self.blog_path, "archives")
            elif option == 'exclude_category':
                try:
                    return self._parser.get('system', option).split(',')
                except ConfigParser.NoOptionError:
                    return []
            elif option == 'post_per_page':
                return int(self._parser.get('system', option))
            elif config_list.has_key(option):
                return self._parser.get(config_list[option], option).decode('utf-8')
            else:
                return self._parser.get('system', option)

config = Config()


class Entry:
    def __init__(self, *initial_data, **kwargs):
        for dictionary in initial_data:
            for key in dictionary:
                setattr(self, key, dictionary[key])
        for key in kwargs:
            setattr(self, key, kwargs[key])


class Category(Entry):
    pass


class TZ(tzinfo):
    def utcoffset(self, dt):
        return timedelta(minutes=int(config.timezone_offset))


class Date(Entry):
    def get_full(self):
        locale_month = config.month.split(',')
        weekdays = config.days.split(',')
        day = int(self.day)
        month = int(self.month) - 1
        weekday = weekdays[self.get_datetime().weekday()]
        return weekday + ', ' + str(day) + ' ' + locale_month[month] + ' ' + str(self.year)

    def get_half(self):
        locale_month = config.month.split(',')
        weekdays = config.days.split(',')
        day = int(self.day)
        month = int(self.month) - 1
        weekday = weekdays[self.get_datetime().weekday()]
        return weekday + ', ' + str(day) + ' ' + locale_month[month]

    def get_iso(self):
        return self.get_datetime().isoformat()

    def get_datetime(self):
        return datetime(
            int(self.year), int(self.month), int(self.day), int(self.minute), int(self.second), 
            tzinfo=TZ()
        )


class Post(Entry):
    def get_full(self):
        return self.content + self.description

    def get_url(self):
        return 'http://' + config.host + '/blog/' + self.date.year + '/' + self.date.month + '/' + self.date.day + '/' + \
            self.name + '/'

    def has_excluded_cats(self):
        for category in self.categories:
            if category.name in config.exclude_category:
                return True
        return False


class Page(Entry):
    pass


class Press:
    def generate(self):
        self.load()
        self.load_first_press()
        print "Load complete."
        self.generate_partially()
        self.gen_articles()
        self.gen_index_range(1)
        self.gen_static()
        print "Generation done."

    def generate_partially(self):
        self.gen_categories()
        self.gen_archives()
        self.gen_feed()
        self.gen_subfeeds()
        print "Generation partially done."

    def load_first_press(self):
        year = datetime.now().year
        for post in self.posts:
            if int(post.date.year) < year:
                year = post.date.year
        self.first_press = year

    def gen_index_by_post(self, post):
        page = self.get_post_page_number(post.name) - 1
        self.gen_index_range(page)

    def gen_post(self, post):
        post_path = path.join(config.blog_path, post.date.year, post.date.month, post.date.day, post.name)
        rendered = self.render('article.html', post=post)
        self.write(post_path, rendered)

    def gen_articles(self):
        for post in self.posts:
            self.gen_post(post)

    def count_posts(self):
        if not self.posts_filtered:
            self.load_posts()
        size = len(self.posts_filtered)
        return size

    def count_pages(self, size):
        pages = math.ceil(float(size)/config.post_per_page) if size > config.post_per_page else 1
        pages = int(pages)
        return pages

    def gen_index_range(self, page, end=None):
        size = self.count_posts()
        pages = self.count_pages(size)
        while page <= pages:
            diff = pages * config.post_per_page - size
            if page == pages:
                offset = 0
                offset_to = config.post_per_page if config.post_per_page < size else size
                to = config.build
            elif page == (pages - 1) and size > config.post_per_page:
                offset = config.post_per_page
                offset_to = (config.post_per_page * (pages + 1 - page)) - diff
                to = path.join(config.blog_path, 'page', str(page))
            else:
                offset = (config.post_per_page * (pages - page)) - diff
                offset_to = offset + config.post_per_page
                to = path.join(config.blog_path, 'page', str(page))
            posts = self.posts_filtered[offset:offset_to]
            rendered = self.render('index.html', posts=posts, page=page, total_pages=pages)
            self.write(to, rendered)

            if page == end:
                break

            page += 1

    def gen_categories(self):
        categories = self.sort_by_categories(self.posts)
        for key, value in categories.iteritems():
            category_path = path.join(config.categories_path, key.lower())
            rendered = self.render('categories.html', years=self.sort_by_years(value), categories=key)
            self.write(category_path, rendered)

    def gen_archives(self):
        rendered = self.render('categories.html', years=self.sort_by_years(self.posts, True))
        self.write(config.archives_path, rendered)

    def gen_feed(self):
        rendered = self.render('atom.xml', posts=self.posts_filtered[0:30], )
        self.write(config.build, rendered, 'atom.xml')

    def gen_subfeeds(self):
        feeds = self.sort_by_feeds(self.posts)
        for key, value in feeds.iteritems():
            category_path = path.join(config.categories_path, key.link.lower())
            rendered = self.render('atom.xml', posts=value, category=key)
            self.write(category_path, rendered, 'atom.xml')

    def gen_static(self):
        for page in self.pages:
            rendered = self.render('page.html', page=page)
            self.write(path.join(config.build, page.name), rendered)

    def render(self, template, **kwargs):
        template = self.get_base_template(template)
        options = {
            'host': config.host,
            'name': config.name,
            'author': config.author,
            'date': "%s&mdash;%s" % (self.first_press, datetime.now().year),
            'pages': self.pages,
            'datetime_now': datetime.now().replace(tzinfo=TZ(), microsecond=0)
        }
        return template.render(dict(kwargs.items() + options.items()))

    def write(self, to, rendered, additional = 'index.html'):
        if not to.endswith('/'):
            to += '/'
        if not path.exists(path.dirname(to)):
            makedirs(path.dirname(to))
        with open(path.join(to, additional), "w") as file:
            file.write(rendered.encode('utf-8', 'ignore'))

    def get_base_template(self, name):
        template = Environment(loader=FileSystemLoader(config.templates))
        return template.get_template(name)

    def sort_by_years(self, posts, archive=False):
        years = {}
        include = {}
        if archive:
            for post in posts:
                if not (post.has_excluded_cats()):
                    include[post.date.year] = True
        for post in posts:
            if not archive or post.date.year in include:
                if not years.has_key(post.date.year):
                    years[post.date.year] = []
                if archive and (post.has_excluded_cats()):
                    pass
                else:
                    years[post.date.year].append(post)
        return reversed(sorted(years.iteritems()))

    def sort_by_categories(self, posts):
        categories = {}
        for post in posts:
            for category in post.categories:
                if not categories.has_key(category.link):
                    categories[category.link] = []
                categories[category.link].append(post)
        return categories

    def sort_by_feeds(self, posts):
        feeds = {}
        for post in posts:
            if not feeds.has_key(post.categories[0]):
                feeds[post.categories[0]] = []
            feeds[post.categories[0]].append(post)
        return feeds


class Octopress(Press):
    posts = []
    posts_filtered = []
    posts_names = []

    def load(self):
        self.load_posts()
        self.load_pages()

    def is_valid_post(self, name):
        if path.isfile(path.join(config.posts, name)) and not name.startswith('.'):
            return True
        return False

    def load_posts(self, end=None):
        self.posts_filtered = []
        self.posts_names = []
        self.posts = []

        posts_list = reversed(listdir(config.posts))

        i = 0
        for f in posts_list:
            if self.is_valid_post(f):
                try:
                    post = self.parse_post(f)
                    if not post.has_excluded_cats():
                        self.posts_filtered.append(post)
                        self.posts_names.append(post.name)
                    self.posts.append(post)
                    i += 1
                    if i == end:
                        break
                except Exception:
                    pass

    def load_pages(self):
        self.pages = [self.parse_page(f) for f in reversed(listdir(config.pages))
            if path.isfile(path.join(config.pages, f)) and not f.startswith('.')
        ]

    def get_post_page_number(self, name):
        size = self.count_posts()
        pages = self.count_pages(size)

        posts_at_second_page = config.post_per_page - (pages * config.post_per_page - size)
        post_index = self.posts_names.index(name)

        post_per_page = int(config.post_per_page)
        page = pages
        pre_main_page = pages - 1

        i = 0
        j = post_per_page

        while page > 0:
            if page == pre_main_page:
                i += j
                j += posts_at_second_page
            elif page != pages:
                i = j
                j += post_per_page
            if j > post_index >= i:
                return page
            page -= 1

    def parse_post(self, name):
        f = file(path.join(config.posts, name), "r")
        post = f.read().decode('utf-8')

        explode = post.split('---')
        settings = explode.pop(1)
        body = "".join(explode)

        body_list = body.split('<!-- more -->')
        content = body_list.pop(0)
        description = "".join(body_list)

        regenerate = False
        auto_sync = re.search('(?<=autosync:).+', settings)
        if auto_sync:
            sync = auto_sync.group(0).strip()
            if sync == 'true':
                regenerate = True

        time_rows = re.search('(?<=time:).+', settings)

        update_time = False
        if time_rows:
            clear_time = time_rows.group(0).strip()
            if clear_time == '-':
                time_rows = ['00', '00']
                update_time = True
            else:
                time_rows = clear_time.split(':')

        categories = re.search('(?<=categories:).+', settings).group(0).strip()
        categories_list = categories.split(',') if (',' in categories) else categories.split()

        for key, item in enumerate(categories_list):
            category_link = category_name = item.strip()
            if '/' in item:
                explode = item.split("/", 1)
                category_link = explode[1].strip()
                category_name = explode[0].strip()
            categories_list[key] = Category(link=category_link, name=category_name)

        link = re.search('(?<=link:).+', settings)
        if link:
            link = link.group(0).strip()

        date = Date(
            config=config,
            year=name[:4],
            month=name[5:7],
            day=name[8:10],
            minute=time_rows[0] if time_rows else 0,
            second=time_rows[1] if time_rows else 0
        )

        return Post(
            config=config,
            date=date,
            file=name,
            name=self.parse_name_from_file(name),
            title=re.search('(?<=title:).+', settings).group(0).strip()[1:-1],
            content=markdown(content, ['extra']),
            description=markdown(description, ['extra']),
            categories=categories_list,
            link=link,
            sync=regenerate,
            update_time=update_time
        )

    def parse_name_from_file(self, name):
        r = re.compile("\d{4}-\d{2}-\d{2}-(.*)\.\w+")
        m = r.match(name)
        if m:
            return m.group(1)

    def parse_page(self, name):
        f = file(path.join(config.pages, name), "r")
        page = f.read().decode('utf-8')
        explode = page.split('---')
        settings = explode.pop(1)
        body = "".join(explode)
        return Page(
            name=name[:-9],
            title=re.search('(?<=title:).+', settings).group(0).strip()[1:-1],
            content=markdown(body, ['extra'])
        )


class PostsEventHandler(FileSystemEventHandler):
    def on_moved(self, event):
        pass

    def on_created(self, event):
        pass

    def on_deleted(self, event):
        pass

    def on_modified(self, event):
        self.generate(event)

    def generate(self, event):
        filename = path.basename(event.src_path)

        if not filename.startswith('.') and not event.is_directory:
            try:
                engine = Octopress()

                if not path.exists(config.build):
                    system("mkdir -p " + config.build)
                    engine.generate()

                post = engine.parse_post(filename)
                engine.load_pages()
                engine.load_posts()
                engine.load_first_press()

                if post.update_time and post.sync:
                    self.update_time(post.file)

                engine.generate_partially()
                engine.gen_index_by_post(post)
                engine.gen_post(post)

                if post.sync:
                    try:
                        cmd = "find /tmp/launch-*/Listeners -type s | head -1"
                        ssh_auth_agent = os.popen(cmd).read().strip()
                        identity_key = path.expanduser(config.key)
                        source = path.join(config.build, '')
                        ssh_args = ['-e', '/usr/bin/ssh -i ' + identity_key]
                        args = ['/usr/bin/rsync', '-a', '-v', '-r'] + ssh_args + [source, config.to]
                        env = dict(os.environ)
                        env['SSH_AUTH_SOCK'] = ssh_auth_agent
                        p = Popen(args, stdout=PIPE, stderr=PIPE, env=env)
                        p.communicate()
                    except Exception:
                        print 'Error running rsync.'

                print 'Post dispatched successfully.'

            except Exception:
                print 'Invalid post format.'

    @staticmethod
    def update_time(filename):
        src = path.join(config.posts, filename)
        with open(src) as f:
            content = f.read()
        current_time = 'time: ' + datetime.now().strftime('%H:%M')
        f = re.compile('(time: -)')
        new = f.sub(current_time, content)
        with open(src, "w") as f:
            f.write(new)


class Server(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            if self.path[-1:] == '/':
                self.path += 'index.html'
            f = open(config.build + unquote(self.path))
            self.send_response(200)
            self.end_headers()
            self.wfile.write(f.read())
            f.close()
            return
        except IOError:
            self.send_error(404, 'File not found.')


def action_sync():
    system("rsync -avrz %s %s" % (config.build + "/*", config.to))
    print('Sync done ...')


def action_preview():
    server_address = ('127.0.0.1', 8080)
    httpd = HTTPServer(server_address, Server)
    print('Http server is running on http://127.0.0.1:8080')
    httpd.serve_forever()


def action_generate():
    engine = Octopress()
    if not path.exists(config.build):
        system("mkdir -p " + config.build)
    system("cp -r " + config.public + "/* " + config.build)
    engine.generate()


def action_gp():
    action_generate()
    action_preview()


def action_gs():
    action_generate()
    action_sync()


def action_init():
    if platform.system() in ('Linux', 'Darwin'):
        src = path.join(get_python_lib(), 'micropress', 'init')
        dst = path.expanduser('~/Documents/Micropress/')

        if not path.exists(dst):
            makedirs(dst, 0755)

            # copy default public sources templates
            system('cp -r ' + path.join(src, '*') + ' ' + dst)
            system('rm -r ' + path.join(dst, 'launch_agents') + ' ' + path.join(dst, 'build'))
            print 'Default src saved in ' + dst

        # copy default config if not found
        config_src = path.join(src, '.microrc')
        config_dst = path.expanduser('~/.microrc')

        if not path.exists(config_dst):
            copyfile(config_src, config_dst)
            print('Config successfully saved in ~/.microrc')

        global config
        config = Config()
        action_generate()

    if platform.system() == 'Darwin':
        launch_agents_src = path.join(src, 'launch_agents')
        launch_agents_dst = '~/Library/LaunchAgents/'

        watcher_name = 'co.fluder.micropress.watcher.plist'
        preview_name = 'co.fluder.micropress.preview.plist'

        watcher_src = path.join(launch_agents_src, watcher_name)
        preview_src = path.join(launch_agents_src, preview_name)

        watcher_dst = path.expanduser(launch_agents_dst + watcher_name)
        preview_dst = path.expanduser(launch_agents_dst + preview_name)

        copyfile(watcher_src, watcher_dst)
        system('launchctl unload -w ' + watcher_dst)
        system('launchctl load -w ' + watcher_dst)
        print "Watcher LaunchAgents rule loaded successful."

        copyfile(preview_src, preview_dst)
        system('launchctl unload -w ' + preview_dst)
        system('launchctl load -w ' + preview_dst)
        print "Preview LaunchAgents rule loaded successful."


def action_watch():
    posts_path = config.posts
    event_handler = PostsEventHandler()
    observer = Observer()
    observer.schedule(event_handler, posts_path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()


def action_add(name=('n', '')):
    template_dst = path.join(config.posts, '') + time.strftime("%Y-%m-%d") + '-' + name + '.md'
    template_src = path.join(get_python_lib(), 'micropress', 'init', 'templates', 'base.markdown')
    copyfile(template_src, template_dst)
    system("vim " + template_dst)


if __name__ == '__main__':
    script.run()