#!/usr/bin/env python3

import sys
import numpy as np
import mmap
import struct
import os


class DarkMatterParticle:
    def __init__(self, pos, vel, acc, mass, ids):
        self.pos = pos
        self.vel = vel
        self.acc = acc
        self.mass = mass
        self.ids = ids


class LogfileReader:
    n_type = 7
    swift_type_dark_matter = 1

    # dtype for the particle's data
    index_dtype = np.dtype([("ids", np.ulonglong), ("offset", np.uint64)])

    def __init__(self, basename):
        self.basename = basename
        logfile = basename + ".dump"
        self.f = open(logfile, "rb")
        self.m = mmap.mmap(self.f.fileno(), 0, prot=mmap.PROT_READ)
        self.read_logfile_header()

    def __del__(self):
        self.m.close()
        self.f.close()

    def read_int(self):
        val = self.m.read(4)
        return int.from_bytes(val, byteorder="little")

    def read_uint(self):
        val = self.m.read(4)
        return struct.unpack("I", val)[0]

    def read_offset(self):
        offset = self.m.read(6)
        return int.from_bytes(offset, byteorder="little")

    def read_string(self):
        string = self.m.read(self.string_length)
        string = struct.unpack("%is" % self.string_length, string)[0]
        i = string.find(b"\x00")
        return string[:i]

    def read_logfile_header(self):
        self.m.seek(0)
        self.vers = self.m.read(20)
        self.major = self.read_int()
        self.minor = self.read_int()
        self.offset_direction = self.read_int()
        self.first_record = self.read_offset()
        self.string_length = self.read_uint()
        self.n_masks = self.read_uint()

        mask_dtype = np.dtype(
            [
                ("name", "U%i" % self.string_length),
                ("value", "int32"),
                ("size", "uint32"),
            ]
        )

        self.masks = np.empty(self.n_masks, dtype=mask_dtype)
        for i in range(self.n_masks):
            self.masks[i]["name"] = self.read_string()
            self.masks[i]["value"] = 1 << i
            self.masks[i]["size"] = self.read_uint()

        print("Fields found (name, mask, size): ", self.masks)

    def read_record_header(self):
        mask = self.m.read(2)
        mask = struct.unpack("h", mask)[0]

        if mask != 316:
            print(mask)

        offset = self.read_offset()
        return mask, offset

    def read_index(self, index_number):
        filename = basename + "_%04i.index" % index_number

        # Read the file
        with open(filename, "rb") as f:
            # read the time
            time = np.fromfile(f, dtype=float, count=1)
            time_int = np.fromfile(f, dtype=np.longlong, count=1)
            print("Time: {}, integer time: {}".format(time[0], time_int[0]))

            # read the number of particles
            nparts = np.fromfile(f, dtype=np.uint64, count=LogfileReader.n_type)

            print("Number of particles:", nparts)

            # read if the file is sorted
            sort = np.fromfile(f, dtype=np.bool, count=1)
            print("File is sorted?", sort[0])

            # read the memory alignment garbage
            n = ((f.tell() + 7) & ~7) - f.tell()
            f.read(n)

            # read the particles
            print("Particles data (ids / offset):")
            data = []
            for n in nparts:
                if n == 0:
                    data.append(None)
                    continue

                tmp = np.fromfile(f, dtype=LogfileReader.index_dtype, count=n)
                data.append(tmp)

            print("\t", data)

            # print the history of new particles
            n_new = np.fromfile(f, dtype=np.uint64, count=LogfileReader.n_type)
            print("New particles: ", n_new)

            new = []
            for n in n_new:
                if n == 0:
                    new.append(None)
                    continue

                new.append(np.fromfile(f, dtype=LogfileReader.index_dtype, count=n))
            print("\t", new)

            # print the history of particles removed
            n_rem = np.fromfile(f, dtype=np.uint64, count=LogfileReader.n_type)
            print("Particles removed: ", n_rem)

            removed = []
            for n in n_rem:
                if n == 0:
                    removed.append(None)
                    continue

                removed.append(np.fromfile(f, dtype=LogfileReader.index_dtype, count=n))
            print("\t", removed)
            return data, new, removed

    def seek(self, new_offset):
        self.m.seek(new_offset)

    def read_particle(self, mask):
        flag = None
        ref = self.masks["name"] == "SpecialFlags"
        if mask & self.masks[ref]["value"]:
            flag = self.m.read(4)
            print("Flag: ", struct.unpack("i", flag)[0])

        pos = None
        ref = self.masks["name"] == "Coordinates"
        if mask & self.masks[ref]["value"]:
            pos = self.m.read(24)
            pos = struct.unpack("ddd", pos)[0]

        vel = None
        ref = self.masks["name"] == "Velocities"
        if mask & self.masks[ref]["value"]:
            vel = self.m.read(12)
            vel = struct.unpack("fff", vel)[0]

        acc = None
        ref = self.masks["name"] == "Accelerations"
        if mask & self.masks[ref]["value"]:
            acc = self.m.read(12)
            acc = struct.unpack("fff", acc)[0]

        # print(pos, vel, acc)
        mass = None
        ref = self.masks["name"] == "Masses"
        if mask & self.masks[ref]["value"]:
            mass = self.m.read(4)
            mass = struct.unpack("f", mass)[0]

        ids = None
        ref = self.masks["name"] == "ParticleIDs"
        if mask & self.masks[ref]["value"]:
            ids = self.m.read(8)
            ids = struct.unpack("q", ids)[0]

        part = DarkMatterParticle(pos, vel, acc, mass, ids)
        return part


if __name__ == "__main__":
    logfile = sys.argv[-1]
    basename = logfile[:-5]

    # Open the logfile
    reader = LogfileReader(basename)

    # Read the first index file
    data, _, _ = reader.read_index(0)

    # Keep only the DM particles
    data = data[LogfileReader.swift_type_dark_matter]

    for i in range(len(data)):
        if i > 10:
            print("Early exit due to low performances")
            break

        off = data["offset"][i]
        off = int(off)
        index_ids = data["ids"][i]
        reader.seek(off)

        for i in range(10):
            mask, offset = reader.read_record_header()
            print(mask, offset, off)

            part = reader.read_particle(mask)
            if index_ids != part.ids:
                raise Exception("Failed", index_ids, part.ids)

            if offset == 0:
                print("End of particle")
                break

            off += offset
            reader.seek(off)
