# Copyright (C) 2017 The Android Open Source Project
#
# 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.

import json
import multiprocessing


TASK_COMMAND = "command"
TASK_SYNC_NETWORK = "sync-network"
TASK_SYNC_LOCAL = "sync-local"


class EventLog:
    """Event log that records events that occurred during a repo invocation.

    Events are written to the log as a consecutive JSON entries, one per line.
    Each entry contains the following keys:
    - id: A ('RepoOp', ID) tuple, suitable for storing in a datastore.
          The ID is only unique for the invocation of the repo command.
    - name: Name of the object being operated upon.
    - task_name: The task that was performed.
    - start: Timestamp of when the operation started.
    - finish: Timestamp of when the operation finished.
    - success: Boolean indicating if the operation was successful.
    - try_count: A counter indicating the try count of this task.

    Optionally:
    - parent: A ('RepoOp', ID) tuple indicating the parent event for nested
        events.

    Valid task_names include:
    - command: The invocation of a subcommand.
    - sync-network: The network component of a sync command.
    - sync-local: The local component of a sync command.

    Specific tasks may include additional informational properties.
    """

    def __init__(self):
        """Initializes the event log."""
        self._log = []
        self._parent = None

    def Add(
        self,
        name,
        task_name,
        start,
        finish=None,
        success=None,
        try_count=1,
        kind="RepoOp",
    ):
        """Add an event to the log.

        Args:
            name: Name of the object being operated upon.
            task_name: A sub-task that was performed for name.
            start: Timestamp of when the operation started.
            finish: Timestamp of when the operation finished.
            success: Boolean indicating if the operation was successful.
            try_count: A counter indicating the try count of this task.
            kind: The kind of the object for the unique identifier.

        Returns:
            A dictionary of the event added to the log.
        """
        event = {
            "id": (kind, _NextEventId()),
            "name": name,
            "task_name": task_name,
            "start_time": start,
            "try": try_count,
        }

        if self._parent:
            event["parent"] = self._parent["id"]

        if success is not None or finish is not None:
            self.FinishEvent(event, finish, success)

        self._log.append(event)
        return event

    def AddSync(self, project, task_name, start, finish, success):
        """Add a event to the log for a sync command.

        Args:
            project: Project being synced.
            task_name: A sub-task that was performed for name.
                One of (TASK_SYNC_NETWORK, TASK_SYNC_LOCAL)
            start: Timestamp of when the operation started.
            finish: Timestamp of when the operation finished.
            success: Boolean indicating if the operation was successful.

        Returns:
            A dictionary of the event added to the log.
        """
        event = self.Add(project.relpath, task_name, start, finish, success)
        if event is not None:
            event["project"] = project.name
            if project.revisionExpr:
                event["revision"] = project.revisionExpr
            if project.remote.url:
                event["project_url"] = project.remote.url
            if project.remote.fetchUrl:
                event["remote_url"] = project.remote.fetchUrl
            try:
                event["git_hash"] = project.GetCommitRevisionId()
            except Exception:
                pass
        return event

    def GetStatusString(self, success):
        """Converst a boolean success to a status string.

        Args:
            success: Boolean indicating if the operation was successful.

        Returns:
            status string.
        """
        return "pass" if success else "fail"

    def FinishEvent(self, event, finish, success):
        """Finishes an incomplete event.

        Args:
            event: An event that has been added to the log.
            finish: Timestamp of when the operation finished.
            success: Boolean indicating if the operation was successful.

        Returns:
            A dictionary of the event added to the log.
        """
        event["status"] = self.GetStatusString(success)
        event["finish_time"] = finish
        return event

    def SetParent(self, event):
        """Set a parent event for all new entities.

        Args:
            event: The event to use as a parent.
        """
        self._parent = event

    def Write(self, filename):
        """Writes the log out to a file.

        Args:
            filename: The file to write the log to.
        """
        with open(filename, "w+") as f:
            for e in self._log:
                json.dump(e, f, sort_keys=True)
                f.write("\n")


# An integer id that is unique across this invocation of the program, to be set
# by the first Add event. We can't set it here since it results in leaked
# resources (see: https://issues.gerritcodereview.com/353656374).
_EVENT_ID = None


def _NextEventId():
    """Helper function for grabbing the next unique id.

    Returns:
        A unique, to this invocation of the program, integer id.
    """
    global _EVENT_ID
    if _EVENT_ID is None:
        # There is a small chance of race condition - two parallel processes
        # setting up _EVENT_ID. However, we expect TASK_COMMAND to happen before
        # mp kicks in.
        _EVENT_ID = multiprocessing.Value("i", 1)
    with _EVENT_ID.get_lock():
        val = _EVENT_ID.value
        _EVENT_ID.value += 1
    return val
