from __future__ import absolute_import

from urllib import urlopen
import base64, sys, os
import tempfile, subprocess
from socket import gethostname

import pylons, tg
from webob.exc import HTTPUnauthorized, HTTPNotFound, HTTPFound

from bazbase import conversion, structure, cache
from bazbase.benchmark import benchmarking
from bazbase import custom as basecust

from . import util, custom
from .translators import url, unurl, edit_url

nothreads = False

# Helper iterator wrapper
def metafirst(obj):
    i = iter(obj)
    q = []
    for s in i:
        if not s.startswith('set_'):
            yield s
        else:
            q.append(s)
    for s in q:
        yield s

def merge(orig, head, mine):
    if not orig.endswith('\n'):
        orig += '\n'
    if not head.endswith('\n'):
        head += '\n'
    if not mine.endswith('\n'):
        mine += '\n'
    
    orig_f = tempfile.NamedTemporaryFile()
    head_f = tempfile.NamedTemporaryFile()
    mine_f = tempfile.NamedTemporaryFile()

    orig_f.write(orig)
    orig_f.flush()
    head_f.write(head)
    head_f.flush()
    mine_f.write(mine)
    mine_f.flush()

    p = subprocess.Popen(['merge', '-p',
                          '-L', 'Current Version',
                          '-L', 'Original',
                          '-L', 'Your Version',
                          head_f.name, orig_f.name, mine_f.name],
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()
    # 1 indicates conflicts
    if p.returncode not in (0, 1):
        raise EnvironmentError(p.returncode, err)

    orig_f.close()
    head_f.close()
    mine_f.close()
    
    return out

def data_for_url(url):
    root = sys.modules[tg.config['application_root_module']]
    contr = root.RootController()
    bits = url.split('/')
    b = bits.pop(0)
    assert not b
    while bits:
        b = bits.pop(0)
        try:
            if hasattr(contr, b + '_'):
                contr = getattr(contr, b + '_')
                continue
            elif hasattr(contr, b):
                contr = getattr(contr, b)
                continue
        except UnicodeEncodeError:
            pass
        bits.insert(0, b)
        break
    response_ext = pylons.request.response_ext
    pylons.request.response_ext = None
    try:
        content_type = pylons.response.headers['Content-type']
    except KeyError:
        content_type = None
    pylons.response.headers['Content-type'] = None
    if hasattr(contr, '_default'):
        res = contr._default(*bits)
    else:
        res = contr(*bits)
    assert pylons.response.headers['Content-type'] is not None
    res_type = pylons.response.headers['Content-type']
    pylons.request.response_ext = response_ext
    if content_type is not None:
        pylons.response.headers['Content-type'] = content_type
    else:
        del pylons.response.headers['Content-type']
    return res, res_type

class Edit(tg.TGController):

    def precache_stuff(self, ename):
        ELEMENTS = 3
        PROPVALS_PER = 8
        try:
            import random
            from bazbase import renderer, db

            with db.begin_transaction():
                elm = structure.get_element(ename)
                if elm is None:
                    # Must have been deleted (or our test's cleaned up).
                    return
                targ = elm.get_ancestors() + elm.get_descendants()
                parent = elm.get_parent()
                if parent is not None:
                    targ += [e for e in elm.get_parent().get_children()
                             if e != elm]
                if len(targ) > ELEMENTS:
                    targ = random.sample(targ, ELEMENTS)
                for t in targ:
                    propnames = t.list_props()
                    if len(propnames) > PROPVALS_PER:
                        propnames = random.sample(propnames, PROPVALS_PER)
                    renderer.start_render(t.ename, propnames, u'html',
                                          priority=-1)

            if basecust.version_control_hook is not None:
                basecust.version_control_hook.prefetch_checkout()
        except:
            type, value, tb = sys.exc_info()
            import traceback
            msg = ''.join(traceback.format_exception(type, value, tb))
            if 'error_subject_prefix' in tg.config:
                util.send_email(msg, subject='Precache %s%s' % (
                    tg.config['error_subject_prefix'], value),
                                from_addr=tg.config['error_email_from'],
                                to_addr=tg.config['email_to'])
            else:
                raise
            

    def start_precache_thread(self, elm):
        import threading
        threading.Thread(target=self.precache_stuff, args=(elm.ename,)).start()

    @tg.expose(template="mako:bazki.templates.edit.element")
    def _default(self, ename="", getattach=None, parent=None, preview=False,
                commit_message=None, **kw):
        with benchmarking('edit controller'):
            try:
                is_post = (pylons.request.method == 'POST')
            except KeyError:
                is_post = False

            if commit_message is not None:
                util.set_commit_message(commit_message)
            mename = unicode(ename, 'utf-8').strip() or u'DocumentationHome'
            if not ename and not pylons.request.url.endswith('/'):
                cprefix = pylons.request.url.split('/')[-1]+'/'
            else:
                cprefix = './'
            canon = url(cprefix+mename.encode('utf-8'))

            if (parent is not None or len(kw) > 0) and not custom.is_editor():
                tg.flash(custom.EDITOR_PRED.message)
                tg.redirect(canon)

            if parent is not None:
                pelm = structure.get_element(parent)
                if pelm is None:
                    tg.flash("You can't make %s a child of the nonexistant element %s!" % (mename,parent))
                    tg.redirect(pylons.url(''))
                else:
                    e = structure.get_element(mename)
                    if e is None:
                        e = pelm.create_child(mename)
                    else:
                        e.set_parent(pelm)

            else:
                e = structure.get_element(mename)
                if e is None:
                    if pylons.request.method == 'POST':
                        tg.redirect(canon)
                    else:
                        #return dict(tg_template='bazki.edit.noelement',
                        #            ename=mename)
                        return tg.render.render(
                            template_vars=dict(ename=mename),
                            template_engine='mako',
                            template_name='bazki.templates.edit.noelement')

            #assert False,(ename,getattach,parent,kw)

            if getattach is not None:
                # Hack to support {{image refs}}
                from . import getting
                if pylons.request.response_ext:
                    prop = getattach
                    ext = pylons.request.response_ext[1:]
                elif '.' not in getattach:
                    prop = getattach
                    ext = 'default'
                else:
                    prop,ext = getattach.split('.')
                return getting.get(mename,ext,e,prop)

            # util.set_current_element(e)
            conflicts = {}
            for key in metafirst(kw):
                if key.startswith('remove_'):
                    pname = unicode(key[len('remove_'):], 'utf-8')
                    head = unicode(e[pname].value, 'utf-8')
                    orig = kw['orig_'+pname].replace('\r\n','\n')
                    if not preview and orig.strip() != head.strip():
                        conflicts[pname] = (orig, head, None, head)
                    else:
                        del e[pname]
                elif key == 'delete':
                    if e.parent is None:
                        tg.flash("You can't delete the root element!")
                    elif e.hasChildren():
                        tg.flash("You can't delete an element with children!")
                    else:
                        st = e.parent
                        e.delete()
                        tg.flash("Successfully deleted.")
                        tg.redirect(pylons.url(cprefix+st.ename.encode('utf-8')))
                elif key == 'rename':
                    newn = kw.get('newname',"").strip()
                    if newn:
                        e.ename = newn
                        canon = pylons.url(cprefix+newn.encode('utf-8'))
                    else:
                        tg.flash("Invalid rename!")
                elif key == 'orgmode':
                    if e.orgmode != kw[key]:
                        e.orgmode = kw[key]
                elif key.startswith('set_'):
                    pname = unicode(key[len('set_'):], 'utf-8')
                    if '.' in pname:
                        sete, setpname = pname.split('.', 1)
                        sete = structure.get_element(sete)
                    else:
                        sete = e
                        setpname = pname
                    if 'remove_'+pname not in kw:
                        # The orig prop is not present for new props.
                        if 'orig_'+pname in kw:
                            orig = kw['orig_'+pname].replace('\r\n','\n')
                            head = unicode(sete.get_prop(setpname), 'utf-8')
                        else:
                            assert setpname not in sete
                            orig = None
                        val = kw[key]
                        if isinstance(val,dict):
                            val = base64.b64decode(val['b64'])
                        elif hasattr(val,'file'):
                            val = val.value
                        else:
                            if isinstance(val,list):
                                val = val[-1]
                            val = (val.replace('\r\n','\n').replace('\r','\n')
                                   .encode('utf-8'))
                        if (not preview and orig is not None
                            and orig.strip() != head.strip()):
                            conflicts[pname] = (orig, head, val,
                                                merge(orig, head, val))
                        else:
                            sete.set_prop(setpname, val)
                # Ignore excess parameters
            if (not preview and len(conflicts) == 0
                and (is_post
                     or not ename or mename != ename 
                     or parent is not None or len(kw) > 0)):
                raise HTTPFound(location=canon)
            elif preview or len(conflicts) > 0:
                ret =  {'element': e, 'preview': True,
                        'args': kw, 'commit_message': commit_message}
                if parent:
                    ret['args']['parent'] = parent
                util.make_preview()
                if len(conflicts) > 0:
                    ret['conflicts'] = conflicts
                    for pname in conflicts:
                        if conflicts[pname][2] is None:
                            del ret['args']['remove_'+pname]
                        else:
                            del ret['args']['orig_'+pname]
                            del ret['args']['set_'+pname]
                    return tg.render.render(
                            template_vars=ret,
                            template_engine='mako',
                            template_name='bazki.templates.edit.merge')
                else:
                    return ret
            else:
                # TODO(xavid): 'twould be safer to start this after the work
                #              for rendering is done.
                if not nothreads:
                    self.start_precache_thread(e)
                
                return {'element': e, 'preview': False,
                        'args':{}, 'commit_message': "%s: " % mename,
                        'nothreads': nothreads}

    @tg.expose()
    def new(self, ename, parent):
        # Post params are unicode, get params are str
        return self._default(ename.encode('utf-8'), parent=parent,
                             commit_message="Added new %s '%s'." % (parent,
                                                                    ename))

    @tg.expose(template="mako:bazki.templates.edit.property")
    def property(self, propname, flavor=None, default=None,
                 visible=None, comment=None,
                 newname=None, rename=None):
        propname = unicode(propname, 'utf-8')
        prop = structure.get_prop(propname)
        if prop is None:
            prop = structure.create_prop(propname, flavor)
        elif flavor is not None:
            prop.set_flavor(flavor)

        util.set_commit_message("Edited prop %s." % propname)        
        
        if default is not None:
            prop.set_default(default)
        if visible is not None:
            prop.set_visible(visible != "False")
        if comment is not None:
            prop.set_comment(comment)
        if rename:
            prop.set_name(newname)

        if pylons.request.method == 'POST' or rename:
            tg.redirect(pylons.url(prop.name.encode('utf-8')))

        return {'prop': prop}

    @tg.expose()
    def mergetest(self):
        test = structure.get_element(u'MergeTest')
        orig = 'The flag should be Maroon!'
        if u'test' in test and test[u'test'].render() == orig:
            orig = 'The flag should be Magenta!'
        return self._default('MergeTest', orig_test=orig, set_test='Blue')

    @tg.expose(template="mako:bazki.templates.edit.test")
    def test(self):
        return {}

    @tg.expose()
    def error(self):
        raise Exception('/edit/error visited')

    @tg.expose()
    def warning(self):
        import warnings
        warnings.warn('/edit/warning visited')
        tg.redirect('/'.join(pylons.request.url.split('/')[:-1]))

    @tg.expose(template="mako:bazki.templates.edit.propvals",
               content_type="text/javascript")
    def propvals(self, ename, format, **propnames):
        from bazbase import renderer
        propnames = [unicode(pn, 'utf-8') for pn in propnames]
        result_map = renderer.wait_for_render(unicode(ename, 'utf-8'),
                                              propnames,
                                              format)
        remainder = [pn for pn in propnames if pn not in result_map]
        return dict(result_map=result_map, ename=ename, format=format,
                    remainder=remainder)

    def go_back(self):
        if pylons.request.referrer:
            dest = unurl(pylons.request.referrer)
        else:
            dest = None
        edit = unurl(pylons.request.url.rsplit('/', 1)[0])
        if (dest is None
            or dest.startswith(edit+'/clear_cache')
            or dest.startswith('/login')):
            tg.redirect(edit)
        else:
            tg.redirect(dest)

    @tg.expose()
    def clear_cache(self):
        # We don't use @require because it binds EDITOR_PRED too early for
        # gameki to monkey-patch it.
        if not custom.EDITOR_PRED.is_met(pylons.request.environ):
            tg.flash(custom.EDITOR_PRED.message)
            raise HTTPUnauthorized()
        conversion.invalidate_cache()
        tg.flash('All cache entries invalidated (from %s on %s).'
                 % (cache
                    .cache_dir(), gethostname()))
        self.go_back()

    @tg.expose()
    def lintify(self):
        # We don't use @require because it binds EDITOR_PRED too early for
        # gameki to monkey-patch it.
        if not custom.EDITOR_PRED.is_met(pylons.request.environ):
            tg.flash(custom.EDITOR_PRED.message)
            raise HTTPUnauthorized()
        util.set_commit_message("Automated yaml cleaning.")
        for e in structure.list_all_elements():
            final_map = e.get_final_map()
            for p in e:
                if p not in final_map:
                    e[p].value = e[p].value
        tg.flash('All .yaml files cleaned up.')
        self.go_back()

    @tg.expose(template="mako:bazki.templates.edit.eval")
    def eval(self, wikitext=''):
        result = None
        wikitext = wikitext.replace('\r\n', '\n')
        if wikitext:
            result = conversion.convert_markup(wikitext, 'html')
        return {'result': result, 'wikitext': wikitext}

    @tg.expose()
    def lipsum(self):
        from .fonts import FONTS_BY_CATEGORY
        
        TEXT = """Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."""

        latex = r"""\documentclass{article}

\usepackage{fontspec}

\begin{document}

"""

        for cat, fonts in FONTS_BY_CATEGORY:
            latex += '\\section{%s}\n\n' % cat.capitalize()
            for f, name, args in fonts:
                latex += '{\\fontspec[%s]{%s}\n' % (args, f)
                latex += '\\subsection{%s}\n' % name
                latex += TEXT
                latex += '\n}\n\n'

        latex += '\\end{document}\n'

        if pylons.request.response_ext == '.tex':
            pylons.response.headers['Content-type'] = 'text/plain'
            return latex
        
        from bazbase.translators import TRANSLATORS, Intermed
        im = Intermed()
        im.setData(latex, '.tex')
        TRANSLATORS['.pdf']['.tex'](im)

        pylons.response.headers['Content-type'] = 'application/pdf'

        return im.asData()
