#!/usr/bin/env python
from durus.file import File
from durus.file_storage import FileStorage
from durus.shelf import Shelf, OffsetMap, read_transaction_offsets
from durus.utils import read_int8, read_int8_str, ShortRead, read, int8_to_str, write_int8, write
from cStringIO import StringIO
import os, sys, time, hashlib, subprocess, random
from datetime import datetime

def write_flush(f, chunk):
    write(f, chunk)
    f.flush()

if __name__ == "__main__":
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option("-d", "--dest", dest="dest",
            help="Durus file to sync TO")
    parser.add_option("-s", "--src", dest="src",
            help="Durus file to sync FROM")
    parser.add_option("-r", "--reader", action="store_true", 
            dest="reader", default=False,
            help="Run as reader.")
    parser.add_option("-o", "--offset", dest="offset", type="int",
            help="Offset of end of the last hash.")

    parser.add_option("-c", "--corrupt", dest="corrupt", action="store_true",
            help="Randomly corrupt transactions sent from the reader.")

    parser.add_option("-n", "--network", dest="network",
            help="The remote user and host for ssh to use to connect to the source.")


    (options, args) = parser.parse_args()

    if not options.reader:
        # writer        
        while True:        
            try:
                if not os.path.exists(options.dest):
                    print "Destination does not exist, copy it"
                    # no local dest exists, get the src
                    if options.network:
                        cmd = """ scp %s:%s %s """ % (options.network, options.src, options.dest)
                    else:
                        cmd = """ cp %s %s """ % (options.src, options.dest)
                    print "cmd:",cmd
                    os.system(cmd)
                    
                    # ensure the file is correct, repair since a partial transaction may be on the end
                    fs = FileStorage(options.dest, repair=True)
                    fs.close()
                
                dest = File(options.dest)
                # is shelf
                if not Shelf.has_format(dest):
                    print "Destination is not a Shelf file"
                    sys.exit(-1)

                # go to hash before end
                dest.seek(-20,2)

                # read 20 bytes, should be hash
                last_hash = read(dest, 20)

                print "Connect reader"
                # open a pipe to a reader
                cmd = """ %s -s %s -r -o %d""" % (sys.argv[0], options.src, len(dest))
                if options.network:
                    cmd = "ssh %s '%s'" % (options.network, cmd)
                print cmd
                
                p = subprocess.Popen(cmd, shell=True, bufsize=0, stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                
                # write the hash
                #write_flush(p.stdin, last_hash)
                
                while True:
                    # read length
                    trans_len = read_int8(p.stdout)

                    # read record data
                    latest_transaction = read(p.stdout, trans_len)
                    trans_data, trans_hash = latest_transaction[:-20], latest_transaction[-20:]
                    
                    assert trans_hash == hashlib.sha1(last_hash + trans_data).digest(), \
                            "Corrupted transaction data read from source, hashes don't match"
                    last_hash = trans_hash
                    
                    # hash matches, write data
                    dest.obtain_lock()
                    write_int8(dest, trans_len)
                    dest.write(latest_transaction)
                    print "Applied transaction at [%s] end=%s" % (datetime.now(), dest.tell())
                    dest.flush()
                    dest.release_lock()

            except ShortRead:
                p.poll()
                dest.close()
                if p.returncode:
                    err = p.stderr.read()
                    print p.returncode, err
                    if p.returncode < 0:
                        sys.exit(p.returncode)
                    else:
                        # backup up local file and get a new copy of the source
                        os.rename(options.dest, options.dest + "-" + datetime.now().strftime("%FT%T"))

    else:
        """
        Read the source and write out chunks of transactions.
        """
        if not os.path.exists(options.src):
            write_flush(sys.stderr, "Source does not exist")
            sys.exit(-1)
        
        src = File(options.src)
    
        # is shelf
        if not Shelf.has_format(src):
            write_flush(sys.stderr, "Source is not a Shelf file")
            sys.exit(-2)

        if options.offset > len(src):
            write_flush(sys.stderr, "Destination file larger than source")
            sys.exit(1)
        
        # compare source hash with passed in value from destination
        src.seek(options.offset)

        while True:
            # read trans
            position = src.tell()

            src_len = os.fstat(src.file.fileno()).st_size
            if position == src_len:
                # see if the src file has been packed, moved, etc.
                fp_inode = os.fstat(src.file.fileno()).st_ino
                name_inode = src.stat().st_ino

                if fp_inode != name_inode:
                    write_flush(sys.stderr, "Source inode has changed")
                    sys.exit(4)
                
                # sleep waiting for more
                time.sleep(1)
            else:
                transaction_start = transaction_end = position
                try:
                    transaction_length = read_int8(src)
                    
                    if transaction_length == 0:
                        # transaction in progress, just 0 length
                        raise ShortRead()
                        
                    transaction_end = transaction_start + 8 + transaction_length
        
                    src.seek(transaction_start)
                    transaction_data = read(src, 8 + transaction_length)
                    
                    if options.corrupt:
                        if random.randint(0, 10000) < 100:
                            # corrupt the data
                            tdata = list(transaction_data)
                            tdata[random.randint(0, transaction_length)] = 'x'
                            transaction_data = "".join(tdata)
                    write_flush(sys.stdout, transaction_data)
                except ShortRead, e:
                    position = src.tell()
                    if position > transaction_start:
                        # go back to the last complete trans
                        src.seek(transaction_start)
