# -*- coding: utf-8 -*-

#
# Copyright (c) 2007-2008 by nexB, Inc. http://www.nexb.com/ - All rights reserved.
# Author: Francois Granade - fg at nexb dot com
# Licensed under the same license as Trac - http://trac.edgewall.org/wiki/TracLicense
#

import os
import pkg_resources
import re
import shutil
import tempfile
import time
import unicodedata

from decimal import Decimal

from trac.core import *
from trac.attachment import AttachmentModule
from trac.core import Component
from trac.perm import IPermissionRequestor
from trac.ticket import TicketSystem
from trac.ticket import model
from trac.util import get_reporter_id
from trac.util.html import html
from trac.web import IRequestHandler
from trac.web.chrome import ITemplateProvider
from trac.web.chrome import add_warning, add_notice
from trac.util.text import to_unicode

from dateutils import *
from dbutils import *
from utils import *
from readers import get_reader


_WORKTIME_COLUMNS = ['ticket', 'worker', 'date', 'hours']

class TracWorkTimeImportModule(Component):

    implements(IRequestHandler, ITemplateProvider)

    # IRequestHandler methods
    def match_request(self, req):
        match = re.match(r'/worktime/importer(?:/([0-9]+))?', req.path_info)
        if match:
            return True

    def process_request(self, req):
        req.perm.assert_permission('WORKTIME_IMPORT_EXECUTE')
        action = req.args.get('action', 'other')

        if req.args.has_key('cancel'):
            req.redirect(req.href('worktime/importer'))
            
        if action == 'upload' and req.method == 'POST':
            req.session['uploadedfile'] = None
            uploadedfilename, uploadedfile = self._save_uploaded_file(req)
            req.session['sheet'] = req.args['sheet']
            req.session['uploadedfile'] = uploadedfile
            req.session['uploadedfilename'] = uploadedfilename
            req.session['tickettime'] = str(int(time.time()))
            return self._do_preview(uploadedfile, int(req.session['sheet']), req)
        elif action == 'import' and req.method == 'POST':
            tickettime = int(req.session['tickettime'])
            if tickettime == 0:
                raise TracError('No time set since preview, unable to import: please upload the file again')

            return self._do_import(req.session['uploadedfile'], int(req.session['sheet']), req, req.session['uploadedfilename'], tickettime)
            
        else:
            req.session['uploadedfile'] = None
            req.session['uploadedfilename'] = None
            imported_rows = req.args.get('rows')
            if imported_rows:
                add_notice(req, u'%s件登録しました。' % imported_rows)

            data = {}

            return 'worktime_importer.html', data, None

    # ITemplateProvider
    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return [('worktime',
                 pkg_resources.resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    # Internal methods

    def _do_preview(self, uploadedfile, sheet, req):
        filereader = get_reader(uploadedfile, sheet)
        try:
            next_page, data = self._process(filereader,
                                            PreviewProcessor(self.env, req, get_reporter_id(req)),
                                            req)
            return next_page, data, None
        finally:
            filereader.close()

    def _do_import(self, uploadedfile, sheet, req, uploadedfilename, tickettime):
        filereader = get_reader(uploadedfile, sheet)
        try:
            try:
                next_page, data = self._process(filereader,
                                     ImportProcessor(self.env, req, get_reporter_id(req)),
                                     req)

                return next_page, data, None
            finally:
                filereader.close()
        except:
            # Unlock the database. This is not really tested, but seems reasonable. TODO: test or verify this
            self.env.get_db_cnx().rollback()
            raise


    def _save_uploaded_file(self, req):
        req.perm.assert_permission('WORKTIME_IMPORT_EXECUTE')
        
        upload = req.args['import-file']
        if not hasattr(upload, 'filename') or not upload.filename:
            raise TracError('No file uploaded')
        if hasattr(upload.file, 'fileno'):
            size = os.fstat(upload.file.fileno())[6]
        else:
            upload.file.seek(0, 2) # seek to end of file
            size = upload.file.tell()
            upload.file.seek(0)
        if size == 0:
            raise TracError("Can't upload empty file")
        
        # Maximum file size (in bytes)
        max_size = AttachmentModule.max_size
        if max_size >= 0 and size > max_size:
            raise TracError('Maximum file size (same as attachment size, set in trac.ini configuration file): %d bytes' % max_size,
                            'Upload failed')
        
        # We try to normalize the filename to unicode NFC if we can.
        # Files uploaded from OS X might be in NFD.
        filename = unicodedata.normalize('NFC', unicode(upload.filename,
                                                        'utf-8'))
        filename = filename.replace('\\', '/').replace(':', '/')
        filename = os.path.basename(filename)
        if not filename:
            raise TracError('No file uploaded')

        return filename, self._savedata(upload.file)


    def _savedata(self, fileobj):

        # temp folder
        tempuploadedfile = tempfile.mktemp()

        flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
        if hasattr(os, 'O_BINARY'):
            flags += os.O_BINARY
        targetfile = os.fdopen(os.open(tempuploadedfile, flags), 'w')
 
        try:
            shutil.copyfileobj(fileobj, targetfile)
        finally:
            targetfile.close()
        return tempuploadedfile

    def _process(self, filereader, processor, req):
        columns, rows = filereader.readers()
        
        columns = [x.lower() for x in columns]

        #カラムチェック
        if len(set(_WORKTIME_COLUMNS) & set(columns)) != len(_WORKTIME_COLUMNS):
            add_warning(req, u'不正なファイル形式です。フィールドは、%s　を定義していください。' % (', '.join(_WORKTIME_COLUMNS)))
            return 'worktime_importer.html', {}
        
        processor.start(columns)
        
        for row in rows:
            processor.do_process(self._parse_row(req, row))
                    
        data = processor.end()
            
        return 'worktime_import_preview.html', data
    
    def _parse_row(self, req, row):
        ticket_id = worker = date = hours = None
        for cell_key in row:
            if cell_key.lower() == 'ticket':
                ticket_id = row[cell_key]
            elif cell_key.lower() == 'worker':
                worker = row[cell_key]
            elif cell_key.lower() == 'date':
                date = row[cell_key]
            elif cell_key.lower() == 'hours':
                hours = row[cell_key]
                
        return {'ticket': ticket_id, 'worker': worker, 'date': date, 'hours': hours}

class ImportProcessor(object):
    
    def __init__(self, env, req, submitter):
        self.env = env
        self.req = req
        self.submitter = submitter
        self.data = {}
        self.rows = []
        self.error_rows = []
        
        # Keep the db to commit it all at once at the end
        self.db = self.env.get_db_cnx()
        
    def start(self, columns):
        self.columns = columns
    
    def do_process(self, row):
        error, row = _validate(self, row)
        if error:
            self.error_rows.append(row)
            return
        else:
            self.rows.append(row)
            update_worktime(self, row['ticket'], row['worker'], self.submitter, row['date'], row['hours'], comments='')
    
    def end(self):
        if len(self.rows) > 0:
            self.db.commit()
            self.req.redirect(self.req.href('worktime/importer', rows=len(self.rows)))
        else:
            add_warning(self.req, u'登録可能なデータが見つかりません。アップロードしたファイルの内容を確認してください。')
            
        self.data.update({'error_rows': self.error_rows})
        return self.data
        
class PreviewProcessor(object):
    
    def __init__(self, env, req, submitter):
        self.env = env
        self.req = req
        self.submitter = submitter
        self.data = {}
        self.rows = []
        self.error_rows = []
        
    def start(self, columns):
        self.columns = columns
    
    def do_process(self, row):
        error, row = _validate(self, row)
        if error:
            self.error_rows.append(row)
        else:
            self.rows.append(row)
    
    def end(self):
        if len(self.rows) > 0:
            add_notice(self.req, u'%s件登録可能です。内容を確認してください。' % len(self.rows))
        else:
            add_warning(self.req, u'登録可能なデータが見つかりません。アップロードしたファイルの内容を確認してください。')
        
        if len(self.error_rows) > 0:
            add_warning(self.req, u'登録不可能なデータが%s件見つかりました。アップロードしたファイルの内容を確認してください。' % len(self.error_rows))
        
        self.data.update({'title': u'プレビュー'})
        self.data.update({'headers': _WORKTIME_COLUMNS})
        self.data.update({'rows': self.rows})
        self.data.update({'error_rows': self.error_rows})
        return self.data
    
def _validate(com, row):
    req = com.req
    env = com.env
    
    for key, cell in row.items():
        if cell is None:
            row['err_message'] = u'%sフィールドに値を設定してください。' % key
            return True, row
    try:
        row['ticket'] = str(int(row['ticket']))
    except:
        row['err_message'] = u'ticketフィールドのフォーマットが不正です。チケット番号を設定してください。'
        return True, row
    
    try:
        row['date'] = _convert_date(req, row['date'])
    except:
        row['err_message'] = u'dateフィールドのフォーマットが不正です。存在する日付を YYYY-MM-DD or YYYY/MM/DD 形式で設定してください。'
        return True, row
    
    try:
        row['hours'] = _convert_hours(row['hours'])
    except:
        row['err_message'] = u'hoursフィールドには時間を設定してください。'
        return True, row
    
    row['worker'] = row['worker'].strip()
    if row['worker'] not in get_all_users(env):
        row['err_message'] = u'workerフィールドには存在するユーザIDを設定してください。'
        return True, row
    
    if not is_related_ticket(env, req, row['ticket'], row['worker']):
        row['err_message'] = u'ユーザ %s はチケット #%s に対して担当者もしくは関連者として登録されていません。' % (row['worker'], row['ticket'])
        return True, row
    
    return False, row
        
    
def _convert_date(req, date):
    '''Excelのシリアル値をyyyy-MM-dd形式に変換する。もし、シリアル値ではない場合はそのままとする 
    '''
    try:
        year = (int(date) - 25569) * 24 * 60 * 60;
        date = format_utcepoch(year)
    except:
        pass
    #一度Dateに変換して文字列に戻す(不正パターンであればエラーとなる)
    date = str_to_datetime(req, date)
    return to_localtime(req, date, format='%Y-%m-%d')

def _convert_hours(hours):
    hours = str(Decimal(str(hours)))
    return hours
        
if __name__ == '__main__': 
    import doctest
    testfolder = __file__
    doctest.testmod()
