# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime
import json
import atexit


class Backend(ABC):
    def __init__(self, verbosity):
        self._verbosity = verbosity

    @property
    def verbosity(self):
        return self._verbosity

    @abstractmethod
    def log(self, timestamp, elapsedtime, step, data):
        pass

    @abstractmethod
    def metadata(self, timestamp, elapsedtime, metric, metadata):
        pass


class Verbosity:
    OFF = -1
    DEFAULT = 0
    VERBOSE = 1


class Logger:
    def __init__(self, backends):
        self.backends = backends
        atexit.register(self.flush)
        self.starttime = datetime.now()

    def metadata(self, metric, metadata):
        timestamp = datetime.now()
        elapsedtime = (timestamp - self.starttime).total_seconds()
        for b in self.backends:
            b.metadata(timestamp, elapsedtime, metric, metadata)

    def log(self, step, data, verbosity=1):
        timestamp = datetime.now()
        elapsedtime = (timestamp - self.starttime).total_seconds()
        for b in self.backends:
            if b.verbosity >= verbosity:
                b.log(timestamp, elapsedtime, step, data)

    def flush(self):
        for b in self.backends:
            b.flush()


def default_step_format(step):
    return str(step)


def default_metric_format(metric, metadata, value):
    unit = metadata["unit"] if "unit" in metadata.keys() else ""
    format = "{" + metadata["format"] + "}" if "format" in metadata.keys() else "{}"
    return "{} : {} {}".format(
        metric, format.format(value) if value is not None else value, unit
    )


def default_prefix_format(timestamp):
    return "DLL {} - ".format(timestamp)


class StdOutBackend(Backend):
    def __init__(
        self,
        verbosity,
        step_format=default_step_format,
        metric_format=default_metric_format,
        prefix_format=default_prefix_format,
    ):
        super().__init__(verbosity=verbosity)

        self._metadata = defaultdict(dict)
        self.step_format = step_format
        self.metric_format = metric_format
        self.prefix_format = prefix_format

    def metadata(self, timestamp, elapsedtime, metric, metadata):
        self._metadata[metric].update(metadata)

    def log(self, timestamp, elapsedtime, step, data):
        print(
            "{}{} {}".format(
                self.prefix_format(timestamp),
                self.step_format(step),
                " ".join(
                    [
                        self.metric_format(m, self._metadata[m], v)
                        for m, v in data.items()
                    ]
                ),
            )
        )

    def flush(self):
        pass


class JSONStreamBackend(Backend):
    def __init__(self, verbosity, filename, append=False):
        super().__init__(verbosity=verbosity)
        self._filename = filename
        self.file = open(filename, "a" if append else "w")
        atexit.register(self.file.close)

    def metadata(self, timestamp, elapsedtime, metric, metadata):
        self.file.write(
            "DLLL {}\n".format(
                json.dumps(
                    dict(
                        timestamp=str(timestamp.timestamp()),
                        elapsedtime=str(elapsedtime),
                        datetime=str(timestamp),
                        type="METADATA",
                        metric=metric,
                        metadata=metadata,
                    )
                )
            )
        )

    def log(self, timestamp, elapsedtime, step, data):
        self.file.write(
            "DLLL {}\n".format(
                json.dumps(
                    dict(
                        timestamp=str(timestamp.timestamp()),
                        datetime=str(timestamp),
                        elapsedtime=str(elapsedtime),
                        type="LOG",
                        step=step,
                        data=data,
                    )
                )
            )
        )

    def flush(self):
        self.file.flush()
