import h5py as h
import numpy as np
import matplotlib
matplotlib.use("Agg")
from pylab import *
import os.path

kernel_gamma = 1.825742
kernel_gamma2 = kernel_gamma * kernel_gamma
kernel_gamma_dim = np.power(kernel_gamma,3)
hydro_dimension_unit_sphere = 4. * np.pi / 3.
kernel_norm = hydro_dimension_unit_sphere * kernel_gamma_dim
error = False

inputFile1 = ""
inputFile2 = ""

# Compare the values of two floats
def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
    return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

# Check list of density neighbours and check that they are correct.
def check_density_neighbours(pids, ngb_ids_naive, ngb_ids_sort, mask, pos,
        h_naive, h_sort, num_invalid, acc):

    for k in range(0,num_invalid):

        # Filter neighbour lists for valid particle ids
        filter_neigh_naive = [i for i in ngb_ids_naive[mask][k] if i > -1]
        filter_neigh_sort = [i for i in ngb_ids_sort[mask][k] if i > -1]

        # Check neighbour lists for differences
        id_list = set(filter_neigh_naive).symmetric_difference(set(filter_neigh_sort))
       
        # Check for duplicate IDs
        duplicate_check_naive = len(filter_neigh_naive) != len(set(filter_neigh_naive))
        duplicate_check_sort = len(filter_neigh_sort) != len(set(filter_neigh_sort))

        if duplicate_check_naive:
            print "Duplicate neighbour ID found in: ", inputFile1
            print filter_neigh_naive
            return True
        
        if duplicate_check_sort:
            print "Duplicate neighbour ID found in: ", inputFile2
            print filter_neigh_sort
            return True

        pid = pids[mask][k]

        # Loop over discrepancies and check if they are actually neighbours
        for pjd in id_list:
            pi_pos = pos[np.where(pids == pid)]
            pj_pos = pos[np.where(pids == pjd)]
            
            hi = h_naive[np.where(pids == pid)]
            
            dx = pi_pos[0][0] - pj_pos[0][0]
            dy = pi_pos[0][1] - pj_pos[0][1]
            dz = pi_pos[0][2] - pj_pos[0][2]
           
            # Correct for BCs
            dx = nearest(dx)
            dy = nearest(dy)
            dz = nearest(dz)

            r2 = dx*dx + dy*dy + dz*dz
            
            hig2 = hi*hi*kernel_gamma2
            
            diff = abs(r2 - hig2)
            
            print "Particle {} is missing {}, hig2: {}, r2: {}, |r2 - hig2|: {}".format(pid,pjd,hig2, r2, diff)
            
            if diff < acc * hig2:
                print "Missing interaction due to precision issue will be ignored."
            else:
                hi_2 = h_sort[np.where(pids == pid)]

                # If a neigbour is missing and the particle has the same h throw
                # an error.
                if(isclose(hi,hi_2)):
                    print "Missing interaction found but particle has the same smoothing length (hi_1: %e, hi_2: %e)."%(hi, hi_2)
                    return True
                else:
                    print "Missing interaction due to different smoothing lengths will be ignored (hi_1: %e, hi_2: %e)."%(hi, hi_2)

    return False

# Check list of force neighbours and check that they are correct.
def check_force_neighbours(pids, ngb_ids_naive, ngb_ids_sort, mask, pos,
        h_naive, h_sort, num_invalid, acc):

    error_val = False

    for k in range(0,num_invalid):

        # Filter neighbour lists for valid particle ids
        filter_neigh_naive = [i for i in ngb_ids_naive[mask][k] if i > -1]
        filter_neigh_sort = [i for i in ngb_ids_sort[mask][k] if i > -1]

        # Check neighbour lists for differences
        id_list = set(filter_neigh_naive).symmetric_difference(set(filter_neigh_sort))
        
        pid = pids[mask][k]

        # Loop over discrepancies and check if they are actually neighbours
        for pjd in id_list:
            pi_pos = pos[np.where(pids == pid)]
            pj_pos = pos[np.where(pids == pjd)]
            
            hi = h_naive[np.where(pids == pid)]
            hj = h_naive[np.where(pids == pjd)]
            
            dx = pi_pos[0][0] - pj_pos[0][0]
            dy = pi_pos[0][1] - pj_pos[0][1]
            dz = pi_pos[0][2] - pj_pos[0][2]
 
            # Correct for BCs
            dx = nearest(dx)
            dy = nearest(dy)
            dz = nearest(dz)
           
            r2 = dx*dx + dy*dy + dz*dz
            
            hig2 = hi*hi*kernel_gamma2
            hjg2 = hj*hj*kernel_gamma2
            
            diff = abs(r2 - max(hig2, hjg2))
            
            print "Particle {} is missing {}, hig2: {}, hjg2: {}, r2: {}, |r2 - max(hig2,hjg2)|: {}".format(pid,pjd,hig2, hjg2, r2, diff)

            if diff < acc * max(hig2,hjg2):
                print "Missing interaction due to precision issue will be ignored."
            else:
                hi_2 = h_sort[np.where(pids == pid)]
                if(isclose(hi,hi_2)):
                    print "Missing interaction due to the same smoothing lengths will not be ignored (hi_1: %e, hi_2: %e)."%(hi, hi_2)
                    error_val = True
                else:
                    print "Missing interaction due to different smoothing lengths will be ignored (hi_1: %e, hi_2: %e)."%(hi, hi_2)

    return error_val

def nearest(dx):
    if(dx > 0.5 * box_size):
        return dx - box_size
    elif(dx < -0.5 * box_size):
        return dx + box_size
    else: 
        return dx

# Parse command line arguments
if len(sys.argv) < 3:
    print "Error: pass input files as arguments"
    sys.exit()
else:
    inputFile1 = sys.argv[1]
    inputFile2 = sys.argv[2]
    if os.path.exists(inputFile1) != 1:
        print "\n{} does not exist!\n".format(inputFile1)
        sys.exit()
    if os.path.exists(inputFile2) != 1:
        print "\n{} does not exist!\n".format(inputFile2)
        sys.exit()

# Open input files    
file_naive = h.File(inputFile1, "r")
file_sort = h.File(inputFile2, "r")

box_size = file_naive["/Header"].attrs["BoxSize"][0]

# Read input file fields
ids_naive = file_naive["/PartType0/ParticleIDs"][:]
ids_sort = file_sort["/PartType0/ParticleIDs"][:]

h_naive = file_naive["/PartType0/SmoothingLength"][:]
h_sort = file_sort["/PartType0/SmoothingLength"][:]

pos_naive = file_naive["/PartType0/Coordinates"][:,:]
#pos_sort = file_sort["/PartType0/Coordinates"][:,:]

num_density_naive = file_naive["/PartType0/Num_ngb_density"][:]
num_density_sort = file_sort["/PartType0/Num_ngb_density"][:]

num_force_naive = file_naive["/PartType0/Num_ngb_force"][:]
num_force_sort = file_sort["/PartType0/Num_ngb_force"][:]

neighbour_ids_density_naive = file_naive["/PartType0/Ids_ngb_density"][:]
neighbour_ids_density_sort = file_sort["/PartType0/Ids_ngb_density"][:]

neighbour_ids_force_naive = file_naive["/PartType0/Ids_ngb_force"][:]
neighbour_ids_force_sort = file_sort["/PartType0/Ids_ngb_force"][:]


#wcount_naive = file_naive["/PartType0/Wcount"][:]
#wcount_sort = file_sort["/PartType0/Wcount"][:]
#
#wcount_naive = wcount_naive * np.power(h_naive,3) * kernel_norm
#wcount_sort = wcount_sort * np.power(h_sort,3) * kernel_norm

# Cross check
max_density_ngbs_naive = np.max(num_density_naive)
max_density_ngbs_sort = np.max(num_density_sort)
max_force_ngbs_naive = np.max(num_force_naive)
max_force_ngbs_sort = np.max(num_force_sort)

print "                   Min     Mean     Max "
print "                   ---------------------"
print "Ngbs density naiv: ", np.min(num_density_naive), np.mean(num_density_naive), max_density_ngbs_naive
print "Ngbs density sort: ", np.min(num_density_sort), np.mean(num_density_sort), max_density_ngbs_sort
print "Ngbs force naiv:   ", np.min(num_force_naive), np.mean(num_force_naive), max_force_ngbs_naive
print "Ngbs force sort:   ", np.min(num_force_sort), np.mean(num_force_sort), max_force_ngbs_sort
#print "Wcount naiv:   ", np.min(wcount_naive), np.mean(wcount_naive), np.max(wcount_naive)
#print "Wcount sort:   ", np.min(wcount_sort), np.mean(wcount_sort), np.max(wcount_sort)

# Sort
index_naive = np.argsort(ids_naive)
index_sort = np.argsort(ids_sort)

num_density_naive = num_density_naive[index_naive]
num_density_sort = num_density_sort[index_sort]
num_force_naive = num_force_naive[index_naive]
num_force_sort = num_force_sort[index_sort]
ids_naive = ids_naive[index_naive]
ids_sort = ids_sort[index_sort]
neighbour_ids_density_naive = neighbour_ids_density_naive[index_naive]
neighbour_ids_density_sort = neighbour_ids_density_sort[index_sort]
neighbour_ids_force_naive = neighbour_ids_force_naive[index_naive]
neighbour_ids_force_sort = neighbour_ids_force_sort[index_sort]
#wcount_naive = wcount_naive[index_naive]
#wcount_sort = wcount_sort[index_sort]
h_naive = h_naive[index_naive]
h_sort = h_sort[index_sort]
pos_naive = pos_naive[index_naive]
#pos_sort = pos_sort[index_sort]

neighbour_length_naive = len(neighbour_ids_density_naive[0])
neighbour_length_sort = len(neighbour_ids_density_sort[0])

# Check that input files are logging the same number of neighbours
if neighbour_length_naive != neighbour_length_sort:
    print "Input files have logged different numbers of neighbour lengths!"
    print "{} has logged: {} neighbours".format(inputFile1, neighbour_length_naive)
    print "{} has logged: {} neighbours".format(inputFile2, neighbour_length_sort)
    exit(1)

if (max_density_ngbs_naive > neighbour_length_naive or max_force_ngbs_naive > neighbour_length_naive or
    max_density_ngbs_sort > neighbour_length_sort or max_force_ngbs_sort > neighbour_length_sort):
    print "The number of neighbours has exceeded the number of neighbours logged."
    print "Modify NUM_OF_NEIGHBOURS in hydro_part.h to log more neighbours."
    print "The highest neighbour count is: ", max(max_density_ngbs_naive,max_force_ngbs_naive, max_density_ngbs_sort,max_force_ngbs_sort)
    exit(1)

# First check
print "\n                         Min    Max"
print "                         ----------"
print "Differences for density:  ", min(num_density_naive - num_density_sort), max(num_density_naive - num_density_sort)
print "Differences for force:    ", min(num_force_naive - num_force_sort), max(num_force_naive - num_force_sort)

# Get the IDs that are different
mask_density = num_density_naive != num_density_sort
mask_force = num_force_naive != num_force_sort
num_invalid_density = np.sum(mask_density)
num_invalid_force = np.sum(mask_force)

print "\nNum non-zero density: ", num_invalid_density
print "Num non-zero force:   ", num_invalid_force

print "\nParticle IDs with incorrect densities"
print "----------------------------------------"
print ids_naive[mask_density]

# Check density neighbour lists
error += check_density_neighbours(ids_naive, neighbour_ids_density_naive,
        neighbour_ids_density_sort, mask_density, pos_naive, h_naive, h_sort,
        num_invalid_density, 2e-6)

print "Num of density interactions", inputFile1
print num_density_naive[mask_density]

print "Num of density interactions", inputFile2
print num_density_sort[mask_density]

print "\nParticle IDs with incorrect forces"
print "------------------------------------"
print ids_naive[mask_force]

# Check force neighbour lists
error += check_force_neighbours(ids_naive, neighbour_ids_force_naive,
        neighbour_ids_force_sort, mask_force, pos_naive, h_naive, h_sort,
        num_invalid_force, 2e-6)

print "Num of force interactions", inputFile1
print num_force_naive[mask_force]

#print "Smoothing lengths", inputFile1
#print h_naive[mask_force]

print "Num of force interactions", inputFile2
print num_force_sort[mask_force]

#print "Smoothing lengths", inputFile2
#print h_sort[mask_force]

# Statistics of h difference
h_relative = (h_naive - h_sort) / h_naive
print "h statistics: {} {} (Min, 1st Percentile)".format(np.min(h_relative), np.percentile(h_relative,1))
print "h statistics: {} {} (Mean, Median)".format(np.mean(h_relative), np.median(h_relative))
print "h statistics: {} {} (Max, 99th Percentile)".format(np.max(h_relative), np.percentile(h_relative, 99))

if error:
    print "\n------------------"
    print "Differences found."
    print "------------------"
    exit(1)
else:
    print "\n---------------------"
    print "No differences found."
    print "---------------------"
    exit(0)
