import os, time, inspect, re, random
import shelve, imp, atexit, urllib
from options import options

from Cheetah.Compiler import Compiler

class NavLink(object):
    """ Used for generating top nav.
    """
    sections = {}
    def __init__(self, (path, name), context):
        self.name = name
        if not path.count('/'): path = '/'
        self.path = path
        self.context = context
        if not self.sections.has_key(path):
            self.sections[path]=context.root().get(path[1:],context.root())

    def active(self):
        """ Pick up whether this nav link should be marked as active or not.
        """
        if self.path == '/':
            if not self.context.dirpath.count('/'):
                return ' id="active"'
        elif self.context.dirpath.count(self.path[:-1]):
            return ' id="active"'
        return ''

    def title(self):
        return self.sections[self.path].title()
        

class SiteMap(object):
    """ All pages need to register with this (via addNode) to be included
        in a the sitemap. The sitemap is rendered grouped by section.
    """

    def __init__(self):
        self.sitemap = {}
        
    def addNode(self,page):
        """ Register a page with the sitemap.
        """
        if page.hidden(): return
        if not ( 'index_html' in page.filenames or
                 'index.html' in page.filenames ): return
        sitemap = self.sitemap
        path = page.site_path()
        parent_path = os.path.dirname(path)
        if path != parent_path:
            title = page.title()
            sitemap = self.sitemap
            node = (path, title)
            sitemap.setdefault(parent_path,[]).append(page)
            if sitemap.has_key(path):
                sitemap.setdefault(parent_path,[]).append(sitemap[path])
                del sitemap[path]

    def addNodes(self,pages):
        addNode = self.addNode
        for page in pages:
            addNode(page)

    def ulist(self,section='/'):
        results = []
        self._ulist(self.sitemap[section][:],1,results)
        return ''.join(results)

    def _ulist(self,section,depth,results):
        results.append("  "*depth)
        if depth > 1: results.append('<li style="list-style-type:none">')
        results.append("<ul>\n")
        
        while section:
            page = section.pop(0)
            if type(page) is list: # sub-section list
                self._ulist(page,depth+1,results)
            else:
                results.append("  "*(depth))
                results.append(('<li>'
                        '<a href="%s" title="Last Modified: %s">%s</a></li>\n')
                        %(page.base_path(), page.last_modified(), page.title()))
        results.append("  "*(depth))
        results.append("</ul>\n")
        if depth > 1: results.append("</li>")

    __str__ = ulist
    

class PTemplates(object):
    """ Manages persistent templates. Stores cheetah generated code and exec's
        it to get the template class/instance. I do this as an optimization, to
        keep the amount of time needed to generate the site down to a minimum.
    """

    namespace = imp.new_module('ptmpl') # exec namespace
    templates = shelve.open(options.persistence,protocol=2)
    atexit.register(templates.close) # flush shelf to disk

    def get(self, template, mtime,
            templates=templates, namespace=namespace.__dict__):
        """
        """
        # name of template class
        template_name = template.replace('/','_')
        # deal with '.' in template name (eg. index.html)
        template_name = template_name.replace('.','_')
        if template_name[-5:]=='.tmpl':
            template_name = template_name[:-5]
        ptmpl = templates.get(template_name,None)
        if ptmpl is None or ptmpl['mtime'] < mtime or options.reload:
            #print len(templates),ptmpl,template
            ptmpl = ptmpl or {}
            source = str(Compiler(file=template,
                    moduleName='ptmpl',mainClassName=template_name))
            ptmpl['source'] = source
            ptmpl['mtime'] = mtime
            templates[template_name] = ptmpl
        exec ptmpl['source'] in namespace
        ptmpl = namespace[template_name]
        return ptmpl()


class PageTemplate(dict):
    """ Namespace object for Cheetah template.
    """

    # defaults for optional variables used in top level template
    keywords = ''
    search = ''
    news = ''
    sitemap = SiteMap()
    # mutable class attribute for tracking handled files on a path
    _handled = {}
    
    def __init__(self, template, dirpath, filenames,
            _getmtime=os.path.getmtime):
        self.template = template
        self.dirpath = dirpath
        self.filenames = filenames
        # id/name of page
        self.basename = os.path.basename(dirpath)
        # tracks files accessed via template
        #self._handled = {}
        self._handled.setdefault(self.dirpath,{})
        # default values
        self.dirnames = []
        # determine template's mtime
        self.mtime = _getmtime(template)

    def setChildren(self,children,page=None):
        """ Sets the subdirectory's pages and set this page as their parent
            page.
        """
        dirnames = self.dirnames
        for child in children:
            child['parent'] = page or self
            child_name = child.basename
            dirnames.append(child_name)
            self[child_name] = child
            
    def pingSitemap(self):
        """ Adds children pages to sitemap. Done here so as to allow 
            for appropriate ordering.
            Also use nav_order so the sitemap corresponds to the top nav.
        """
        nav_order = []
        for dirname in self.dirnames:
            child = self[dirname]
            nav_order.append((str(child.get('nav_order',dirname)),dirname))
        nav_order.sort()
        self.dirnames = dirnames = [n for o,n in nav_order]
        self.sitemap.addNodes([self[c] for c in dirnames])
        if self.dirpath == '/':
            self.sitemap.addNode(self)

    def get(self,name,_default=KeyError):
        """ Lazily instantiate page templates for files, put them in the dict
            cache and set the as handled. Track handled to know what files
            aren't being used as templates and just need to be copied over 
            (eg. images).
        """
        if self.has_key(name):
            return dict.get(self,name)
        for fname in [name,('%s.html'%name),('%s.tmpl'%name)]:
            if fname in self.filenames:
                self._handled[self.dirpath][name]=None
                filepath = os.path.join(self.dirpath,fname)
                page = PageTemplate(filepath, self.dirpath, self.filenames)
                page.setChildren([self[c] for c in self.dirnames],self)
                #page._handled = self._handle
                page['parent']= dict.get(self,'parent',None)
                self[fname]=page # cache
                return page
        if _default is not KeyError:
            return _default
        raise KeyError(name)
    __getitem__=get

    #######################################################################
    ### For generating
    #######################################################################
    def __repr__(self):
        """ Useful when debugging
        """
        return "<%s:%s:%s>" % (self.__class__.__name__,self.dirpath,self.keys())

    def __str__(self, ptemplates_get=PTemplates().get):
        """ Cheetah uses __str__ as the method to render variables.
        """
        #print 'respond',self.dirpath,self.template
        cache_id = (self.dirpath,self.template)
        if not self.has_key(cache_id):
            tmpl = ptemplates_get(self.template,self.mtime)
            # this was the previous version that worked with cheetah 1.x
            # had to change it to below for 2.0. don't have 1.0 anymore 
            # so not sure if it is compatible.
            #tmpl._searchList.insert(0,self)
            tmpl.searchList().insert(0,self)
            self[cache_id] = tmpl.respond()
        return dict.get(self,cache_id)

    def render(self, dest_root):
        """ Render template and copy auxilary files
        """
        #print 'render',self.dirpath
        rootless_dirpath = '/'.join((self.dirpath.split('/')[1:]))
        savepath = os.path.join(dest_root,rootless_dirpath)
        if not os.path.exists(savepath):
            os.mkdir(savepath)
        savefile = os.path.join(savepath,'index.html')
        # 
        if self.get('index_html',None):
            try:
                open(savefile,'w').write(str(self))
            except:
                if os.path.exists(savefile):
                    os.remove(savefile) # remove bad savefile
                print 'not rendered',savefile
                raise
        # needs to be after respond() call
        self.copy_files(savepath) # copy over auxilary files
        self.clean_cruft(savepath) # remove unneeded files

    def clean_cruft(self, path, join=os.path.join):
        """ Remove files from dest that are no longer in source.
          
            Should this remove _handled files (that used to be unhandled)?
        """
        for filename in os.listdir(path):
            if filename == 'index.html':
                continue
            if filename in self.filenames or filename in self.dirnames:
                continue
            filepath = os.path.join(path,filename)
            if not os.access(filepath,os.W_OK):
                continue
            if os.path.isdir(filepath):
                for dp,dns,fns in os.walk(filepath,topdown=False):
                    for fn in fns:
                        os.remove(join(dp,fn))
                        #print 'removing',join(dp,fn)
                    for dn in dns:
                        os.rmdir(join(dp,dn))
                        #print 'removing',join(dp,dn)
                #print 'removing',filepath
                os.rmdir(filepath)
            else:
                #print 'removing',filepath
                os.remove(filepath)

    def copy_files(self, savepath,
            _template=os.path.basename(options.template)):
        """ Copy non-template files.
        """
        def _update_if_modified(source,dest, modified = os.path.getmtime):
            if not os.path.exists(dest) or modified(source)>modified(dest):
                #print 'overwriting',dest,'with',source
                open(dest,'w').write(open(source).read())
        basename = os.path.basename
        join = os.path.join
        for filename in self.unhandled():
            # in case you want your template under root dir
            if filename == _template: continue
            source = join(self.dirpath,filename)
            dest = join(savepath,filename)
            exists = os.path.exists
            islink = os.path.islink
            if islink(source): # support sym-links
                linked_source = os.readlink(source)
                local_source = join(self.dirpath,basename(linked_source))
                if exists(local_source) and not islink(local_source):
                    source = basename(linked_source)
                    if not os.path.exists(dest):
                        os.symlink(source,dest)
                else:
                    _update_if_modified(linked_source,dest)
            else:
                _update_if_modified(source,dest)

    def unhandled(self):
        """ Auxilary files not used directly in templates (eg. images).
        """
        handled = self._handled[self.dirpath]
        for filename in self.filenames:
            if not handled.has_key(filename):
                yield filename

    def base_path(self):
        """ Path for page rooted for site. 
            With trailing slash (base href style).
        """
        return self.site_path() + '/'

    def site_path(self):
        """ Path for page rooted for site.
            Used to build paths in the templates.
        """
        if not self.dirpath.count('/'):
            return '/'
        return self.dirpath[self.dirpath.find('/'):]

    #######################################################################
    ### Used from templates 
    #######################################################################
    def title(self):
        """ Always want a title, even if not explicitally set.
        """
        if not self.has_key('title'):
            self['title'] = str(self.get('title',
                    self.basename.capitalize())).strip()
        return self['title']

    def breadcrumbs(self,links=1):
        """ Trail from root of site.
        """
        if not self.dirpath.count('/'): return ''
        crumbs = [self.title()]
        parent = self.get('parent',None)
        while parent is not None:
            if links:
                crumbs.append(parent.link())
            else:
                crumbs.append(parent.title())
            parent = parent.get('parent',None)
        crumbs.reverse()
        return crumbs

    def link(self):
        """ Relative link to this page.
        """
        return '<a href="%s">%s</a>' % (self.base_path(),self.title())

    def top_nav(self,_memo={}):
        """ Generate top nav.
        """
        if not _memo.has_key('nav_info'):
            root = self.root()
            nav_pages = []
            pages = [root]
            while pages:
                page = pages.pop()
                pages.extend([page.get(c) for c in page.dirnames])
                if page.filenames.has_key('nav_text'):
                    nav_pages.append(page)
            nav_info = []
            for page in nav_pages:
                nav_path = page.base_path()
                order = str(page['nav_order']).strip()
                text = str(page['nav_text'])
                nav_info.append((order,nav_path,text))
            nav_info.sort()
            nav_info.reverse()
            _memo['nav_info']=nav_info

        return [NavLink((p,h),self) for (o,p,h) in _memo['nav_info']]
        
    def current_date(self,format="%b %d %Y"):
        return time.strftime(format,time.localtime())
    
    def last_modified(self,
            format=" %Y.%m.%d", opt_tmpl=options.template):
        pjoin = os.path.join
        realpath = os.path.realpath
        getmtime = os.path.getmtime
        if self.template == opt_tmpl:
            for index in ['index_html','index.html','index.tmpl']:
                index = realpath(pjoin(self.dirpath,index))
                if os.path.exists(index): break
            else:
                # warning
                print "Missing index file", self.dirpath, index
        else:
            index = realpath(pjoin(self.dirpath,self.template))
        files = [index]
        files.extend(pjoin(self.dirpath,f) for f in self._handled[self.dirpath])
        mtime = max(getmtime(realpath(f)) for f in files)
        return time.strftime(format,time.localtime(mtime))
    
    def year(self,format="%Y"):
        return time.strftime(format,time.localtime())
     
    def menus(self):
        """ Return all files that start with 'menu'. Use this as a
            way to get content for right menus.
        """
        menus = [fn for fn in self.filenames
                if fn.startswith('menu')]
        menus.sort()
        return [self.get(m) for m in menus]

    findnames = re.compile('<a name="(.+)">(.+)</a>',re.I).findall
    def name_links(self, findnames=findnames):
        """ #name style links
        """
        link = '<a href="#%s">%s</a>'
        return [link % (name,title)
                for name,title in findnames(str(self.get('index_html')))]

    def subdirs(self,order=None):
        """ All children/subdirectory pages.
        """
        dirnames = self.dirnames
        if order:
            dirnames = dirnames[:]
            try: # order is list in desired order
                dirnames.sort(lambda x,y: cmp(order.index(x),order.index(y)))
            except ValueError:
                print 'Error ordering subdirs for: %s' % self.dirpath
        for dirname in dirnames:
            if not self[dirname].hidden():
                yield self[dirname]

    def subdir_links(self,order=None):
        """ links for sub-directories
        """
        return [subdir.link() for subdir in self.subdirs(order=order)]

    def root(self,_cache={}):
        """ Root page.
        """
        if not _cache.has_key('__root'):
            parent = self
            while parent.get('parent',None) is not None:
                parent = parent['parent']
            _cache['__root']=parent
        return _cache['__root']

    getquotes = re.compile('<blockquote>(.*?)(<p.*?)</blockquote>',re.S).findall
    def random_quote(self,getquotes=getquotes):
        """ I have a file full of various quotes I like. This lets me grab a
            random one for display.
        """
        quotes = self.root().get('quotes')
        quote,cite = random.choice(getquotes(str(quotes)))
        quote = ('<blockquote class="quote"><a href="%s">%s</a>%s</blockquote>'
                % (quotes.base_path(),quote,cite))
        return quote

    def validate(self):
        w3 = "http://validator.w3.org/check?uri=%s"
        local = "http://zhar.net%s" % self.base_path()
        return w3 % urllib.quote(local,safe='')

    def validate_css(self):
        w3 = "http://jigsaw.w3.org/css-validator/validator?uri=%s"
        local = "http://zhar.net/site.css"
        return w3 % urllib.quote(local,safe='')

    def hidden(self,hide='hide'):
        """ Mark page as hidden. Ie. can be used while rendering other 
            pages, but doesn't render out itself. Depends on catch in process.
        """
        page = self
        while page is not None:
            if hide in page.filenames:
                return 1
            page = page.get('parent',None)
        return 0
        
