#!/usr/bin/python3
# -*- coding: utf-8 -*-

#  Copyright © 2015  B. Clausius <barcc@gmx.de>
#
#  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 sys, os
sys.path.insert(0, '.')
from glob import glob
from subprocess import call, Popen, PIPE
from difflib import unified_diff
import pickle
import pickletools
from pprint import pformat
from io import BytesIO, StringIO

from pybiklib.config import MODELS_DIR
from pybiklib.model import Model


pformat_kwargs = {'compact': False}
try:
    pformat_kwargs['width'] = os.environ['COLUMNS']
except KeyError:
    pass
    
    
def pager(lines):
    process = Popen('less', stdin=PIPE, universal_newlines=True)
    try:
        process.stdin.writelines(lines)
    except BrokenPipeError:
        pass
    else:
        process.stdin.close()
    finally:
        process.wait()
        
def bindiff(bytes1, bytes2):
    for i, (a, b) in enumerate(zip(bytes1, bytes2)):
        if a != b:
            print('      first difference in byte', i+1)
            break
            
def pickle_disassembly(bytes):
    lines = []
    strio = StringIO()
    pickletools.dis(bytes, out=strio, annotate=1)
    lines += strio.getvalue().splitlines(keepends=True)
    #for opcode, arg, pos in pickletools.genops(bytes):
    #    lines.append('{2:4}: {0.name} arg:{1!r}\n'.format(opcode, arg, pos))
    #    lines.append('      {0.doc}\n'.format(opcode))
    return lines
    
def show_diff(filename1, filename2, bytes1, bytes2):
    len1 = len(bytes1)
    len2 = len(bytes2)
    if len1 != len2:
        print('    file contents have different length:', len1, len2)
    if bytes1 != bytes2:
        print('    file contents are different')
        bindiff(bytes1, bytes2)
        
    buffer1 = BytesIO(bytes1)
    data1 = pickle.load(buffer1)
    text1 = pformat(data1, **pformat_kwargs)
    dlen1 = buffer1.tell()
    vlen1 = len1 - dlen1
    vectors1 = Model.read_vectors(buffer1) if vlen1 else []
    
    buffer2 = BytesIO(bytes2)
    data2 = pickle.load(buffer2)
    text2 = pformat(data2, **pformat_kwargs)
    dlen2 = buffer2.tell()
    vlen2 = len2 - dlen2
    vectors2 = Model.read_vectors(buffer2) if vlen2 else []
    
    lines = []
    
    if dlen1 != dlen2:
        print('    data parts have different length:', dlen1, dlen2)
    if bytes1[:dlen1] != bytes2[:dlen2]:
        print('    data parts are different')
        bindiff(bytes1[:dlen1], bytes2[:dlen2])
        dis1 = pickle_disassembly(bytes1[:dlen1])
        dis2 = pickle_disassembly(bytes2[:dlen2])
        lines += unified_diff(dis1, dis2, filename1, filename2, 'pickle disassembly')
    if text1 != text2:
        print('    data parts have different content')
        lines += unified_diff(text1.splitlines(), text2.splitlines(), filename1, filename2, 'data')
    if vlen1 != vlen2:
        print('    vector parts have different length:', vlen1, vlen2)
    if bytes1[dlen1:] != bytes2[dlen2:]:
        print('    vector parts are different')
        bindiff(bytes1[dlen1:], bytes2[dlen2:])
        vectors1 = [str(v)+'\n' for v in vectors1]
        vectors2 = [str(v)+'\n' for v in vectors2]
        lines += unified_diff(vectors1, vectors2, filename1, filename2, 'vectors')
        
    if lines:
        pager(lines)
        
def build_models(args):
    call('./setup.py build_models -i --reproducible'.split() + args)
    
def compare_models(dirname1, dirname2):
    assert os.path.exists(dirname1)
    assert os.path.exists(dirname2)
    filenames1 = os.listdir(dirname1)
    filenames2 = os.listdir(dirname2)
    print('comparing', dirname1, 'and', dirname2)
    for filename in sorted(set(filenames1 + filenames2)):
        if filename not in filenames1:
            print(' ', filename, 'not in', dirname1)
        elif filename not in filenames2:
            print(' ', filename, 'not in', dirname2)
        else:
            fn1 = os.path.join(dirname1, filename)
            fn2 = os.path.join(dirname2, filename)
            with open(fn1, 'rb') as f1, open(fn2, 'rb') as f2:
                s1 = f1.read()
                s2 = f2.read()
            if s1 != s2:
                print(' ', filename)
                show_diff(fn1, fn2, s1, s2)
                
def main():
    args = sys.argv[1:]
    if not args:
        print('usage:', os.path.basename(sys.argv[0]), 'n [buildargs]')
        print('build models n times and check whether they are the same')
        return
    count = int(args[0])
    args = args[1:]
    prevdirname = None
    i = -1
    for i, dirname in enumerate(sorted(glob(MODELS_DIR + '-*'))):
        curdirname = MODELS_DIR + '-{:04}'.format(i + 1)
        if curdirname != dirname and not os.path.exists(curdirname):
            os.rename(dirname, curdirname)
        else:
            curdirname = dirname
        if prevdirname is not None:
            compare_models(prevdirname, curdirname)
        prevdirname = curdirname
    if os.path.exists(MODELS_DIR) and prevdirname is not None:
        compare_models(prevdirname, MODELS_DIR)
    i += 1
    for unused_i in range(i, i + count):
        if os.path.exists(MODELS_DIR):
            i += 1
            prevdirname = MODELS_DIR + '-{:04}'.format(i)
            os.rename(MODELS_DIR, prevdirname)
        build_models(args)
        if not os.path.exists(MODELS_DIR):
            print('build failed')
            break
        if prevdirname is not None:
            compare_models(prevdirname, MODELS_DIR)
        
if __name__ == '__main__':
    main()
    

