#!/usr/bin/python
# Copyright 2011  Lars Wirzenius
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import cliapp
import ConfigParser
import logging
import os
import sys


class Table(object):

    '''Represent tabular data for formatting purposes.'''
    
    sep = '  '
    
    def __init__(self):
        self.caption = None
        self.columns = []
        self.rows = []

    def add_column(self, heading1, heading2, format, left):
        self.columns.append((heading1, heading2, format, left))

    def add_row(self, data):
        assert len(data) == len(self.columns)
        self.rows.append(data)

    def write_plaintext(self, f):
        if self.caption:
            f.write('%s\n%s\n\n' % (self.caption, '-' * len(self.caption)))
        
        cells = []
        cells.append([h1 for h1, h2, format, left in self.columns])
        cells.append([h2 for h1, h2, format, left in self.columns])
        for row in self.rows:
            cells.append([self.format_cell(row[i], self.columns[i][2])
                          for i in range(len(self.columns))])

        widths = self.compute_column_widths(cells)
        
        f.write('    %s\n' % self.format_headings(widths, 0))
        f.write('    %s\n' % self.format_headings(widths, 1))
        f.write('\n')
        for row in self.rows:
            f.write('    %s\n' % self.format_row(row, widths))

    def align_cell(self, width, text, column):
        h1, h2, fmt, left = self.columns[column]
        if left:
            return '%-*s' % (width, text)
        else:
            return'%*s' % (width, text)

    def format_cell(self, data, format):
        return format % data

    def compute_column_widths(self, cells):
        widths = [0] * len(self.columns)
        for row in cells:
            for i, data in enumerate(row):
                widths[i] = max(widths[i], len(data))
        return widths

    def format_headings(self, widths, which):
        headings = [self.align_cell(widths[i], self.columns[i][which], i)
                    for i in range(len(widths))]
        return self.sep.join(headings)
        
    def format_row(self, row, widths):
        cells = [self.align_cell(widths[i], self.columns[i][2] % row[i], i)
                 for i in range(len(widths))]
        return self.sep.join(cells)


class SeivotsSummary(cliapp.Application):

    def process_args(self, args):
        seivots = []
        for filename in args:
            try:
                seivots.append(self.read_seivot(filename))
            except ConfigParser.NoSectionError:
                sys.stderr.write('Ignoring %s: not a proper seivot file\n' %
                                 filename)
            
        for group, caption in self.find_groups(seivots):
            title = 'Profile: %s' % caption
            self.output.write('%s\n%s\n\n' % (title, '-' * len(title)))
            ops = ['backup', 'restore', 'list_files', 'forget', 'verify',
                   'fsck']
            for op in ops:
                self.output.write('Operation: %s\n\n' % op)
                table = self.make_table(group, op)
                table.write_plaintext(self.output)
                self.output.write('\n')

    def make_table(self, group, op):
        cols = {
            'obnam-revno': ('obnam', 'revno', '%s', self.get_obnam_revision),
            'larch-revno': ('larch', 'revno', '%s', self.get_larch_revision),
            'gen0-speed': ('gen0', 'Mbit/s', '%.1f', self.get_gen0_speed),
            'gen0-time': ('gen0', 's', '%.1f', self.get_gen0_time),
            'gen0-ram': ('gen0', 'MiB', '%.1f', self.get_gen0_ram),
            'slowest-speed': ('slowest', 'Mbit/s', '%.1f', 
                              self.get_slowest_speed),
            'slowest-time': ('slowest', 's', '%.1f', self.get_slowest_time),
            'biggest-ram': ('biggest', 'MiB', '%.1f', self.get_biggest_ram),
            'repo-size': ('repo size', 'MiB', '%.1f', self.get_repo_size),
            'repo-writes': ('r-writes', 'MiB', '%.1f', self.get_repo_writes),
            'repo-reads': ('r-reads', 'MiB', '%.1f', self.get_repo_reads),
            'branch': ('branch', '', '%s', self.get_branch),
            'description': ('desc', '', '%s', self.get_description),
        }
        
        leftist = ('branch', 'description')
        
        prefix = ('obnam-revno', 'larch-revno')
        which = {
            'backup': 
                ('gen0-time', 'gen0-speed', 'gen0-ram', 
                 'slowest-time', 'slowest-speed', 'biggest-ram',
                 'repo-size', 'repo-writes', 'repo-reads'),
            'restore': 
                ('gen0-time', 'gen0-speed', 'gen0-ram', 'slowest-time',
                 'slowest-speed', 'biggest-ram','repo-writes', 'repo-reads'),
            'list_files': 
                ('gen0-time', 'gen0-ram', 'slowest-time', 'biggest-ram',
                 'repo-writes', 'repo-reads'),
            'forget': 
                ('gen0-time', 'gen0-ram', 'slowest-time', 'biggest-ram',
                 'repo-writes', 'repo-reads'),
            'fsck': 
                ('gen0-time', 'gen0-ram', 'slowest-time', 'biggest-ram',
                 'repo-writes', 'repo-reads'),
            'verify': 
                ('gen0-time', 'gen0-speed', 'gen0-ram', 'slowest-time',
                 'slowest-speed', 'biggest-ram','repo-writes', 'repo-reads'),
        }
        suffix = ('branch', 'description',)
        colnames = prefix + which[op] + suffix
        
        table = Table()
        for colname in colnames:
            h1, h2, fmt, func = cols[colname]
            table.add_column(h1, h2, fmt, colname in leftist)
        
        for seivot in group:
            row = []
            for colname in colnames:
                logging.debug('colname: %s' % colname)
                h1, h2, fmt, func = cols[colname]
                value = func(seivot, op)
                logging.debug('value: %s' % repr(value))
                row.append(value)
            table.add_row(row)

        return table

    def read_seivot(self, filename):
        cp = ConfigParser.ConfigParser()
        cp.read([filename])
        cp.set('meta', 'branch', os.path.basename(os.path.dirname(filename)))
        return cp

    def find_groups(self, seivots):
        # We group together seivot files that share the following:
        # - initial size
        # - incremental size
        # - profile name
        # - encryption used

        def key(s):
            init = self.getint(s, '0', 'backup.new-data')
            inc = self.getint(s, '1', 'backup.new-data')
            prof = self.get(s, 'meta', 'profile-name', default='unknown')
            enc = self.get(s, 'meta', 'encrypted', default='no')
            
            return init, inc, prof, enc

        def caption(s):
            init, inc, prof, enc = key(s)
            return ('%s %.0f/%.0f MiB (%s)' %
                     (prof, self.disksize(init), self.disksize(inc),
                      'encrypted' if enc == 'yes' else 'unencrypted'))

        groups = {}
        for seivot in seivots:
            k = key(seivot)
            groups[k] = groups.get(k, []) + [seivot]

        groups = sorted(groups.values(), key=lambda g: key(g[0]))

        for group in groups:
            group.sort(key=self.getkey)

        return [(group, caption(group[0])) for group in groups]

    def get(self, seivot, group, key, default=None):
        if seivot.has_option(group, key):
            return seivot.get(group, key)
        else:
            return default

    def getint(self, seivot, group, key):
        return int(self.get(seivot, group, key, default=0))

    def getfloat(self, seivot, group, key):
        return float(self.get(seivot, group, key, default=0.0))

    def getkey(self, seivot):
        return (self.get(seivot, 'meta', 'revision'), 
                 self.getint(seivot, 'meta', 'larch-revision'))

    def get_size(self, seivot):
        return self.getint(seivot, '0', 'backup.new-data')

    def find_initial_sizes(self, seivots):
        return list(set(self.get_size(s) for s in seivots))

    def find_seivots_in_size_group(self, seivots, size):
        return [s for s in seivots if self.get_size(s) == size]

    def values(self, op, suffix, seivot):
        for section in seivot.sections():
            if section not in ['meta', '0']:
                yield (section, 
                       self.getfloat(seivot, section, '%s.%s' % (op,suffix)))

    def find_slowest_incremental(self, op, seivot):
        v = list(self.values(op, 'real', seivot))
        if not v:
            return '0'
        return min(v, key=lambda pair: pair[1])[0]

    def get_obnam_revision(self, seivot, op):
        return self.get(seivot, 'meta', 'revision', default='unknown')

    def get_larch_revision(self, seivot, op):
        return self.get(seivot, 'meta', 'larch-revision', default='unknown')
        
    def get_branch(self, seivot, op):
        return self.get(seivot, 'meta', 'branch', default='unknown')
        
    def get_description(self, seivot, op):
        return self.get(seivot, 'meta', 'description', default='')

    def get_gen0_speed(self, seivot, op):
        bytes = self.getfloat(seivot, '0', 'backup.new-data')
        secs = self.get_gen0_time(seivot, op)
        return self.xferspeed(bytes, secs)

    def get_gen0_time(self, seivot, op):
        return self.getfloat(seivot, '0', '%s.real' % op)

    def get_gen0_ram(self, seivot, op):
        return self.ramsize(self.getfloat(seivot, '0', '%s.maxrss' % op) * 
                            1024)

    def get_slowest_speed(self, seivot, op):
        gen = self.find_slowest_incremental(op, seivot)
        bytes = self.getfloat(seivot, gen, 'backup.new-data')
        secs = self.getfloat(seivot, gen, '%s.real' % op)
        return self.xferspeed(bytes, secs)

    def get_slowest_time(self, seivot, op):
        gen = self.find_slowest_incremental(op, seivot)
        secs = self.getfloat(seivot, gen, '%s.real' % op)
        return secs

    def get_biggest_ram(self, seivot, op):
        pair = min(self.values(op, 'maxrss', seivot), 
                   key=lambda pair: pair[1])
        kilobytes = float(pair[1])
        return self.ramsize(kilobytes * 1024)

    def get_repo_size(self, seivot, op):
        return self.disksize(self.getfloat(seivot, '0', 'backup.new-data'))

    def get_repo_writes(self, seivot, op):
        bytes = self.getfloat(seivot, '0', '%s.repo-bytes-written' % op)
        return self.disksize(bytes)

    def get_repo_reads(self, seivot, op):
        bytes = self.getfloat(seivot, '0', '%s.repo-bytes-read' % op)
        return self.disksize(bytes)

    def xferspeed(self, bytes, seconds):
        if seconds > 0:
            return 8.0 * bytes / seconds / (1000**2)
        else:
            return 0

    def ramsize(self, bytes):
        return float(bytes) / (1024**2)

    def disksize(self, bytes):
        return float(bytes) / (1024**2)


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

