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

"""
This file is part of the web2py Web Framework
Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
"""

import sys
import cPickle
import traceback
import types
import os
import datetime
import logging

from utils import web2py_uuid
from storage import Storage
from http import HTTP
from html import BEAUTIFY

logger = logging.getLogger("web2py")

__all__ = ['RestrictedError', 'restricted', 'TicketStorage', 'compile2']

class TicketStorage(Storage):

    """
    defines the ticket object and the default values of its members (None)
    """

    def __init__(
        self,
        db=None,
        tablename='web2py_ticket'
        ):
        self.db = db
        self.tablename = tablename

    def store(self, request, ticket_id, ticket_data):
        """
        stores the ticket. It will figure out if this must be on disk or in db
        """
        if self.db:
            self._store_in_db(request, ticket_id, ticket_data)
        else:
            self._store_on_disk(request, ticket_id, ticket_data)

    def _store_in_db(self, request, ticket_id, ticket_data):
        table = self._get_table(self.db, self.tablename, request.application)
        table.insert(ticket_id=ticket_id,
                     ticket_data=cPickle.dumps(ticket_data),
                     created_datetime=request.now)
        logger.error('In FILE: %(layer)s\n\n%(traceback)s\n' % ticket_data)

    def _store_on_disk(self, request, ticket_id, ticket_data):
        ef = self._error_file(request, ticket_id, 'wb')
        try:
            cPickle.dump(ticket_data, ef)
        finally:
            ef.close()

    def _error_file(self, request, ticket_id, mode, app=None):
        root = request.folder
        if app:
            root = os.path.join(os.path.join(root, '..'), app)
        errors_folder = os.path.abspath(os.path.join(root, 'errors'))#.replace('\\', '/')
        return open(os.path.join(errors_folder, ticket_id), mode)

    def _get_table(self, db, tablename, app):
        tablename = tablename + '_' + app
        table = db.get(tablename, None)
        if table is None:
            db.rollback()   # not necessary but one day
                            # any app may store tickets on DB
            table = db.define_table(
                tablename,
                db.Field('ticket_id', length=100),
                db.Field('ticket_data', 'text'),
                db.Field('created_datetime', 'datetime'),
                )
        return table

    def load(
        self,
        request,
        app,
        ticket_id,
        ):
        if not self.db:
            ef = self._error_file(request, ticket_id, 'rb', app)
            try:
                return cPickle.load(ef)
            finally:
                ef.close()
        table = self._get_table(self.db, self.tablename, app)
        rows = self.db(table.ticket_id == ticket_id).select()
        if rows:
            return cPickle.loads(rows[0].ticket_data)
        return None


class RestrictedError(Exception):
    """
    class used to wrap an exception that occurs in the restricted environment
    below. the traceback is used to log the exception and generate a ticket.
    """

    def __init__(
        self,
        layer='',
        code='',
        output='',
        environment=None,
        ):
        """
        layer here is some description of where in the system the exception
        occurred.
        """
        if environment is None: environment = {}
        self.layer = layer
        self.code = code
        self.output = output
        self.environment = environment
        if layer:
            try:
                self.traceback = traceback.format_exc()
            except:
                self.traceback = 'no traceback because template parting error'
            try:
                self.snapshot = snapshot(context=10,code=code,
                                         environment=self.environment)
            except:
                self.snapshot = {}
        else:
            self.traceback = '(no error)'
            self.snapshot = {}

    def log(self, request):
        """
        logs the exception.
        """

        try:
            d = {
                'layer': str(self.layer),
                'code': str(self.code),
                'output': str(self.output),
                'traceback': str(self.traceback),
                'snapshot': self.snapshot,
                }
            ticket_storage = TicketStorage(db=request.tickets_db)
            ticket_storage.store(request, request.uuid.split('/',1)[1], d)
            return request.uuid
        except:
            logger.error(self.traceback)
            return None


    def load(self, request, app, ticket_id):
        """
        loads a logged exception.
        """
        ticket_storage = TicketStorage(db=request.tickets_db)
        d = ticket_storage.load(request, app, ticket_id)

        self.layer = d['layer']
        self.code = d['code']
        self.output = d['output']
        self.traceback = d['traceback']
        self.snapshot = d.get('snapshot')


def compile2(code,layer):
    """
    The +'\n' is necessary else compile fails when code ends in a comment.
    """
    return compile(code.rstrip().replace('\r\n','\n')+'\n', layer, 'exec')

def restricted(code, environment=None, layer='Unknown'):
    """
    runs code in environment and returns the output. if an exception occurs
    in code it raises a RestrictedError containing the traceback. layer is
    passed to RestrictedError to identify where the error occurred.
    """
    if environment is None: environment = {}
    environment['__file__'] = layer
    try:
        if type(code) == types.CodeType:
            ccode = code
        else:
            ccode = compile2(code,layer)
        exec ccode in environment
    except HTTP:
        raise
    except Exception, error:
        # XXX Show exception in Wing IDE if running in debugger
        if __debug__ and 'WINGDB_ACTIVE' in os.environ:
            etype, evalue, tb = sys.exc_info()
            sys.excepthook(etype, evalue, tb)
        raise RestrictedError(layer, code, '', environment)

def snapshot(info=None, context=5, code=None, environment=None):
    """Return a dict describing a given traceback (based on cgitb.text)."""
    import os, types, time, traceback, linecache, inspect, pydoc, cgitb

    # if no exception info given, get current:
    etype, evalue, etb = info or sys.exc_info()

    if type(etype) is types.ClassType:
        etype = etype.__name__

    # create a snapshot dict with some basic information
    s = {}
    s['pyver'] = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
    s['date'] = time.ctime(time.time())

    # start to process frames
    records = inspect.getinnerframes(etb, context)
    s['frames'] = []
    for frame, file, lnum, func, lines, index in records:
        file = file and os.path.abspath(file) or '?'
        args, varargs, varkw, locals = inspect.getargvalues(frame)
        call = ''
        if func != '?':
            call = inspect.formatargvalues(args, varargs, varkw, locals,
                    formatvalue=lambda value: '=' + pydoc.text.repr(value))

        # basic frame information
        f = {'file': file, 'func': func, 'call': call, 'lines': {}, 'lnum': lnum}

        highlight = {}
        def reader(lnum=[lnum]):
            highlight[lnum[0]] = 1
            try: return linecache.getline(file, lnum[0])
            finally: lnum[0] += 1
        vars = cgitb.scanvars(reader, frame, locals)

        # if it is a view, replace with generated code
        if file.endswith('html'):
            lmin = lnum>context and (lnum-context) or 0
            lmax = lnum+context
            lines = code.split("\n")[lmin:lmax]
            index = min(context, lnum) - 1

        if index is not None:
            i = lnum - index
            for line in lines:
                f['lines'][i] = line.rstrip()
                i += 1

        # dump local variables (referenced in current line only)
        f['dump'] = {}
        for name, where, value in vars:
            if name in f['dump']: continue
            if value is not cgitb.__UNDEF__:
                if where == 'global': name = 'global ' + name
                elif where != 'local': name = where + name.split('.')[-1]
                f['dump'][name] = pydoc.text.repr(value)
            else:
                f['dump'][name] = 'undefined'

        s['frames'].append(f)

    # add exception type, value and attributes
    s['etype'] = str(etype)
    s['evalue'] = str(evalue)
    s['exception'] = {}
    if isinstance(evalue, BaseException):
        for name in dir(evalue):
            # prevent py26 DeprecatedWarning:
            if name!='message' or sys.version_info<(2.6):
                value = pydoc.text.repr(getattr(evalue, name))
                s['exception'][name] = value

    # add all local values (of last frame) to the snapshot
    s['locals'] = {}
    for name, value in locals.items():
        s['locals'][name] = pydoc.text.repr(value)

    # add web2py environment variables
    for k,v in environment.items():
        if k in ('request', 'response', 'session'):
            s[k] = BEAUTIFY(v)

    return s



