#    Copyright 2016 ARM Limited
#
# 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 collections

try:
    import pandas as pd
except ImportError:
    pd = None

SurfaceFlingerFrame = collections.namedtuple('SurfaceFlingerFrame', 'desired_present_time actual_present_time frame_ready_time')
GfxInfoFrame = collections.namedtuple('GfxInfoFrame', 'Flags IntendedVsync Vsync OldestInputEvent NewestInputEvent HandleInputStart AnimationStart PerformTraversalsStart DrawStart SyncQueued SyncStart IssueDrawCommandsStart SwapBuffers FrameCompleted')
# https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/libs/hwui/JankTracker.cpp
# Frames that are exempt from jank metrics.
# First-draw frames, for example, are expected to be slow,
# this is hidden from the user with window animations and other tricks
# Similarly, we don't track direct-drawing via Surface:lockHardwareCanvas() for now
# Android M: WindowLayoutChanged | SurfaceCanvas
GFXINFO_EXEMPT = 1 | 4

VSYNC_INTERVAL = 16666667


class FpsProcessor(object):
    """
    Provides common object for processing surfaceFlinger output for frame
    statistics.

    This processor returns the four frame statistics below:

        :FPS: Frames Per Second. This is the frame rate of the workload.
        :frame_count: The total number of frames rendered during the execution of
                 the workload.
        :janks: The number of "janks" that occurred during execution of the
                workload. Janks are sudden shifts in frame rate. They result
                in a "stuttery" UI. See http://jankfree.org/jank-busters-io
        :not_at_vsync: The number of frames that did not render in a single
                       vsync cycle.
    """

    def __init__(self, data, action=None, extra_data=None):
        """
        data         - a pandas.DataFrame object with frame data (e.g. frames.csv)
        action       - output metrics names with additional action specifier
        extra_data   - extra data given to use for calculations of metrics
        """
        self.data = data
        self.action = action
        self.extra_data = extra_data

    def process(self, refresh_period, drop_threshold):  # pylint: disable=too-many-locals
        """
        Generate frame per second (fps) and associated metrics for workload.

        refresh_period - the vsync interval
        drop_threshold - data points below this fps will be dropped
        """
        fps = float('nan')
        frame_count, janks, not_at_vsync = 0, 0, 0
        vsync_interval = refresh_period
        per_frame_fps = pd.Series()

        # SurfaceFlinger Algorithm
        if self.data.columns.tolist() == list(SurfaceFlingerFrame._fields):
            # fiter out bogus frames.
            bogus_frames_filter = self.data.actual_present_time != 0x7fffffffffffffff
            actual_present_times = self.data.actual_present_time[bogus_frames_filter]
            actual_present_time_deltas = actual_present_times.diff().dropna()

            vsyncs_to_compose = actual_present_time_deltas.div(vsync_interval)
            vsyncs_to_compose.apply(lambda x: int(round(x, 0)))

            # drop values lower than drop_threshold FPS as real in-game frame
            # rate is unlikely to drop below that (except on loading screens
            # etc, which should not be factored in frame rate calculation).
            per_frame_fps = (1.0 / (vsyncs_to_compose.multiply(vsync_interval / 1e9)))
            keep_filter = per_frame_fps > drop_threshold
            filtered_vsyncs_to_compose = vsyncs_to_compose[keep_filter]
            per_frame_fps.name = 'fps'

            if not filtered_vsyncs_to_compose.empty:
                total_vsyncs = filtered_vsyncs_to_compose.sum()
                frame_count = filtered_vsyncs_to_compose.size

                if total_vsyncs:
                    fps = 1e9 * frame_count / (vsync_interval * total_vsyncs)

                janks = self._calc_janks(filtered_vsyncs_to_compose)
                not_at_vsync = self._calc_not_at_vsync(vsyncs_to_compose)

        # GfxInfo Algorithm
        elif self.data.columns.tolist() == list(GfxInfoFrame._fields):
            frame_time = self.data.FrameCompleted - self.data.IntendedVsync
            per_frame_fps = (1e9 / frame_time)
            keep_filter = per_frame_fps > drop_threshold
            per_frame_fps = per_frame_fps[keep_filter]
            per_frame_fps.name = 'fps'

            frame_count = self.data.index.size
            if frame_count:
                janks = frame_time[frame_time >= vsync_interval].count()
                not_at_vsync = self.data.IntendedVsync - self.data.Vsync
                not_at_vsync = not_at_vsync[not_at_vsync != 0].count()

                if frame_count > 1:
                    duration = self.data.Vsync.iloc[-1] - self.data.Vsync.iloc[0]
                    fps = (1e9 * frame_count) / float(duration)

            # If gfxinfocsv is provided, get stats from that instead
            if self.extra_data:
                series = pd.read_csv(self.extra_data, header=None, index_col=0, squeeze=True)
                if not series.empty:  # pylint: disable=maybe-no-member
                    frame_count = series['Total frames rendered']
                    janks = series['Janky frames']
                    not_at_vsync = series['Number Missed Vsync']

        metrics = (fps, frame_count, janks, not_at_vsync)
        return per_frame_fps, metrics

    def percentiles(self):
        # SurfaceFlinger Algorithm
        if self.data.columns.tolist() == list(SurfaceFlingerFrame._fields):
            frame_time = self.data.frame_ready_time.diff()
        # GfxInfo Algorithm
        elif self.data.columns.tolist() == list(GfxInfoFrame._fields):
            frame_time = self.data.FrameCompleted - self.data.IntendedVsync

        data = frame_time.dropna().quantile([0.90, 0.95, 0.99])
        # Convert to ms, round to nearest, cast to int
        data = data.div(1e6).round()
        try:
            data = data.astype('int')
        except ValueError:
            pass

        # If gfxinfocsv is provided, get stats from that instead
        if self.extra_data:
            series = pd.read_csv(self.extra_data, header=None, index_col=0, squeeze=True)
            if not series.empty:  # pylint: disable=maybe-no-member
                data = series[series.index.str.contains('th percentile')]  # pylint: disable=maybe-no-member

        return list(data.get_values())

    @staticmethod
    def _calc_janks(filtered_vsyncs_to_compose):
        """
        Internal method for calculating jank frames.
        """
        pause_latency = 20
        vtc_deltas = filtered_vsyncs_to_compose.diff().dropna()
        vtc_deltas = vtc_deltas.abs()
        janks = vtc_deltas.apply(lambda x: (pause_latency > x > 1.5) and 1 or 0).sum()

        return janks

    @staticmethod
    def _calc_not_at_vsync(vsyncs_to_compose):
        """
        Internal method for calculating the number of frames that did not
        render in a single vsync cycle.
        """
        epsilon = 0.0001
        func = lambda x: (abs(x - 1.0) > epsilon) and 1 or 0
        not_at_vsync = vsyncs_to_compose.apply(func).sum()

        return not_at_vsync