#
#    This file is a part of the PetitPas Project
#    Copyright (C) 2014  Francois Mazen
#
#    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 subprocess
import json
import os

solver_path = 'bin/Debug/PetitPas'

# the tuple for each test file is: (input_file, expected_result_file, result_file_format)
input_file_names = [
                    ('2DTriangles_input.json', '2DTriangles_expected.json'),
                    ('2DTriangles_input.json', '2DTriangles_expected.vtk', 'vtk'),
                    ('3DTetrahedrons_input.json', '3DTetrahedrons_expected.json'),
                    ('3DTetrahedrons_input.json', '3DTetrahedrons_expected.vtk', 'vtk'),
                    ('2DTriangles_zero_displacement_input.json', '2DTriangles_expected.json')
                   ]
testDirectory = 'tests'

def test_file(file_names):
    '''Launch a solver instance with input/output file names from file_names tuple.'''

    # Prepare the file names for input and output.
    input_file_name = os.path.join(testDirectory, file_names[0])
    output_file_name = input_file_name + '.output'
    expected_file_name = os.path.join(testDirectory, file_names[1])
    if len(file_names) == 3:
        result_file_format = file_names[2]
    else:
        result_file_format = None

    arguments = [solver_path, '-i', input_file_name, '-o', output_file_name]
    if result_file_format:
        arguments.append('-of')
        arguments.append(result_file_format)

    # Launch test and check the result.
    print 'Launching: ' + input_file_name
    launch_solver(arguments)
    check_result_file(output_file_name, expected_file_name, result_file_format)



def launch_solver(arguments):
    '''Launch PetitPas solver with the given arguments and check that the return code is 0 (=OK).'''

    return_code = subprocess.call(arguments)

    if return_code:
        print 'The solver exited with code ' + str(return_code)
        exit(1)

def write_error_and_exit(expected_file_name, output_file_name, error_message):
    '''Write a generic error message and the given error_message, then exit the program.'''

    print 'The solver output is not as expected.'
    print 'Reference file: ' + expected_file_name
    print 'Output file: ' + output_file_name
    print error_message
    exit(1)

def check_result_file(output_file_name, expected_file_name, result_file_format):
    '''Check that output_file_name and expected_file_name are similar.'''

    # JSON
    if result_file_format == 'json' or not result_file_format:
        check_json_results(expected_file_name, output_file_name)

    # Default case: compare text file lines by lines.
    else:
        check_text_files_with_tolerance(expected_file_name, output_file_name)


def check_json_results(expected_file_name, actual_file_name):
    '''Process the two json files and check the values with tolerance.'''

    with open(actual_file_name, 'r') as fp:
        actual_json_object = json.load(fp)

    with open(expected_file_name, 'r') as fp:
        expected_json_object = json.load(fp)

    for result_index, expected_result in enumerate(expected_json_object['results']):

        # Check result types
        expected_type = expected_result['type']
        actual_result = actual_json_object['results'][result_index]
        if actual_result['type'] != expected_type:
            write_error_and_exit(expected_file_name, actual_file_name, 'Wrong result type.')

        if len(expected_result['values']) != len(actual_result['values']):
            write_error_and_exit(expected_file_name, actual_file_name, 'Wrong number of values.')

        # Check values
        for value_index, value in enumerate(expected_result['values']):

            actual_value = actual_result['values'][value_index]

            if 'node_id' in value:
                if value['node_id'] != actual_value['node_id']:
                    write_error_and_exit(expected_file_name, actual_file_name, 'Wrong node id.')

            if 'element_id' in value:
                if value['element_id'] != actual_value['element_id']:
                    write_error_and_exit(expected_file_name, actual_file_name, 'Wrong element id.')

            actual_tensor = actual_value['values']
            for tensor_index, tensor_value in enumerate(value['values']):
                if abs(tensor_value - actual_tensor[tensor_index]) > 1e-12:
                    write_error_and_exit(expected_file_name, actual_file_name, 'Wrong tensor value.')

def check_text_files_with_tolerance(expected_file_name, actual_file_name):
    '''Compares given text file line by line and if there is a difference,
       it tries to find float values and compare them with a tolerance.'''

    with open(actual_file_name, 'r') as fp:
        output_lines = fp.readlines()

    with open(expected_file_name, 'r') as fp:
        expected_lines = fp.readlines()

    if len(output_lines) != len(expected_lines):
        write_error_and_exit(expected_file_name, output_file_name, 'Wrong number of lines.')

    for line_index in range(len(output_lines)):
        if output_lines[line_index] != expected_lines[line_index]:

            # Try to split the lines and find numerical data, in order to test them with tolerance.
            expected_values = expected_lines[line_index].split()
            output_values = output_lines[line_index].split()

            value_count = len(expected_values)
            if value_count == len(output_values):
                for value_index in range(value_count):
                    expected = expected_values[value_index]
                    actual = output_values[value_index]

                    # Find the problematic value
                    if expected != actual:

                        if (not is_float(expected)) or (not is_float(actual)):
                            write_error_and_exit(expected_file_name, output_file_name, 'Difference at line ' + str(line_index + 1))

                        # Check with tolerance
                        if abs(float(expected) - float(actual)) > 1e-12:
                            write_error_and_exit(expected_file_name, output_file_name, 'Difference at line ' + str(line_index + 1))
            else:
                write_error_and_exit(expected_file_name, output_file_name, 'Difference at line ' + str(line_index + 1))

def is_float(string_value):
    '''Returns True is string_value is a float representation.'''
    try:
        float(string_value)
        return True
    except ValueError:
        return False

if __name__ == '__main__':
    for file_name in input_file_names:
        test_file(file_name)
