"""Implementation of odin_data Meta Writer

This module is passed meta data messages for a single acquisition which it writes to disk.
Will need to be subclassed by detector specific implementation.

Matt Taylor, Diamond Light Source
"""
import os
from time import time
import logging

import h5py

from odin_data import _version as versioneer
from odin_data.util import construct_version_dict
from .hdf5dataset import HDF5Dataset, Int64HDF5Dataset

# Data message parameters
FRAME = "frame"
OFFSET = "offset"
RANK = "rank"
CREATE_DURATION = "create_duration"
WRITE_DURATION = "write_duration"
FLUSH_DURATION = "flush_duration"
CLOSE_DURATION = "close_duration"
MESSAGE_TYPE_ID = "parameter"

# Configuration items
DIRECTORY = "directory"
FILE_PREFIX = "file_prefix"
FLUSH_FRAME_FREQUENCY = "flush_frame_frequency"
FLUSH_TIMEOUT = "flush_timeout"


def require_open_hdf5_file(func):
    """A decorator to verify the HDF5 file is open before calling the wrapped method

    If the HDF5 file is currently open for writing, call the method, else log the reason
    the file is not open

    NOTE: This should only be used on MetaWriter methods (that take self as the first
    argument)

    """
    def wrapper(*args, **kwargs):
        writer = args[0]  # Extract class instance (self) from args

        if writer.file_open:
            # It is safe to call the wrapped method
            return func(*args, **kwargs)

        # It is not safe to call the wrapped method - log the reason why
        if writer.finished:
            reason = "Already finished writing"
        else:
            reason = "Have not received startacquisition yet"
        writer._logger.error(
            "%s | Cannot call %s - File not open - %s",
            writer._name,
            func.__name__,
            reason,
        )

    return wrapper


class MetaWriter(object):
    """This class handles meta data messages and writes parameters to disk"""

    FILE_SUFFIX = "_meta.h5"
    CONFIGURE_PARAMETERS = [
        DIRECTORY,
        FILE_PREFIX,
        FLUSH_FRAME_FREQUENCY,
        FLUSH_TIMEOUT,
    ]
    # Detector-specific parameters received on per-frame meta message
    DETECTOR_WRITE_FRAME_PARAMETERS = []

    def __init__(self, name, directory, process_count):
        """
        Args:
            name(str): Unique name to construct file path and to include in
                       log messages
            directory(str): Directory to create the meta file in
            process_count(int): Total number of processes we will receive data from

        """
        self._logger = logging.getLogger(self.__class__.__name__)

        # Config
        self.directory = directory
        self.file_prefix = None
        self.flush_frame_frequency = 100
        self.flush_timeout = 1

        # Status
        self.full_file_path = None
        self.write_count = 0
        self.finished = False
        self.write_timeout_count = 0

        # Internal parameters
        self._name = name
        self._processes_running = [False] * process_count
        self._last_flushed = time()  # Seconds since epoch
        self._frames_since_flush = 0
        self._hdf5_file = None
        self._datasets = dict(
            (dataset.name, dataset)
            for dataset in self._define_datasets() + self._define_detector_datasets()
        )
        # Child class parameters
        self._frame_data_map = dict()  # Map of frame number to detector data
        self._writers_finished = False
        self._detector_finished = True  # See stop_when_detector_finished

    @staticmethod
    def _define_datasets():
        return [
            Int64HDF5Dataset(FRAME),
            Int64HDF5Dataset(OFFSET),
            Int64HDF5Dataset(CREATE_DURATION, cache=False),
            Int64HDF5Dataset(WRITE_DURATION),
            Int64HDF5Dataset(FLUSH_DURATION),
            Int64HDF5Dataset(CLOSE_DURATION, cache=False),
        ]

    @staticmethod
    def _define_detector_datasets():
        return []

    @property
    def file_open(self):
        return self._hdf5_file is not None

    @property
    def active_process_count(self):
        return self._processes_running.count(True)

    def _generate_full_file_path(self):
        prefix = self.file_prefix if self.file_prefix is not None else self._name
        self.full_file_path = os.path.join(
            self.directory, "{}{}".format(prefix, self.FILE_SUFFIX)
        )
        return self.full_file_path

    def _create_file(self, file_path, dataset_size):
        self._logger.debug(
            "%s | Opening file %s - Expecting %d frames",
            self._name,
            file_path,
            dataset_size,
        )

        try:
            self._hdf5_file = h5py.File(file_path, "w", libver="latest")
        except IOError as error:
            self._logger.error(
                "%s | Failed to create file:\n%s: %s",
                self._name,
                error.__class__.__name__,
                error.message,
            )
            return

        self._create_datasets(dataset_size)
        # Datasets created after this point will not be SWMR-readable
        self._hdf5_file.swmr_mode = True

    @require_open_hdf5_file
    def _close_file(self):
        self._logger.info("%s | Closing file", self._name)

        self._flush_datasets()
        self._hdf5_file.close()
        self._hdf5_file = None

    @require_open_hdf5_file
    def _create_datasets(self, dataset_size):
        """Add predefined datasets to HDF5 file and store handles

        Args:
            datasets(list(HDF5Dataset)): The datasets to add to the file

        """
        self._logger.debug("%s | Creating datasets", self._name)

        for dataset in self._datasets.values():
            dataset_handle = self._hdf5_file.create_dataset(
                name=dataset.name,
                shape=dataset.shape,
                maxshape=dataset.maxshape,
                dtype=dataset.dtype,
                fillvalue=dataset.fillvalue,
            )
            dataset.initialise(dataset_handle, dataset_size)

    @require_open_hdf5_file
    def _add_dataset(self, dataset_name, data, dataset_size=None):
        """Add a new dataset with the given data

        Args:
            dataset_name(str): Name of dataset
            data(np.ndarray): Data to initialise HDF5 dataset with
            dataset_size(int): Dataset size - required if more data will be added

        """
        self._logger.debug("%s | Adding dataset %s", self._name, dataset_name)

        if dataset_name in self._datasets:
            self._logger.debug(
                "%s | Dataset %s already created",
 self._name, dataset_name
            )
            return

        self._logger.debug(
            "%s | Creating dataset %s with data:\n%s", self._name, dataset_name, data
        )
        dataset = HDF5Dataset(dataset_name, dtype=None, fillvalue=None, cache=False)
        dataset_handle = self._hdf5_file.create_dataset(name=dataset_name, data=data)
        dataset.initialise(dataset_handle, dataset_size)

        self._datasets[datase
t_name] = dataset

    @require_open_hdf5_file
    def _add_value(self, dataset_name, value, offset=0):
        """Append a value to the named dataset

        Args:
            dataset_name(str): Name of dataset
            value(): The value to append
            index(int): The offset to add the value to

        """
        self._logger.debug("%s | Adding value to %s", self._name, dataset_name)

        if dataset_name not in self._datasets:
            self._logger.error("%s | No such dataset %s", self._name, dataset_name)
            return

        self._datasets[dataset_name].add_value(value, offset)

    @require_open_hdf5_file
    def _add_values(self, expected_parameters, data, offset):
        """Take values of parameters from data and write to datasets at offset

        Args:
            expected_parameters(list(str)): Parameters to write
            data(dict): Set of parameter values
            offset(int): Offset to write parameters to in datasets

        """
        self._logger.debug("%s | Adding values to datasets", self._name)

        for 