#!/usr/bin/python
#
# Copyright 2016 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#

"""Generates an HTML file with plot of buffer level in the audio thread log."""

import argparse
import collections
import logging
import string

page_content = string.Template("""
<html meta charset="UTF8">
<head>
  <!-- Load c3.css -->
  <link href="https://rawgit.com/masayuki0812/c3/master/c3.css" rel="stylesheet" type="text/css">
  <!-- Load d3.js and c3.js -->
  <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
  <script src="https://rawgit.com/masayuki0812/c3/master/c3.js" charset="utf-8"></script>
  <style type="text/css">
    .c3-grid text {
        fill: grey;
    }
    .event_log_box {
      font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
      font-size: 20px;
      font-style: normal;
      font-variant: normal;
      font-weight: 300;
      line-height: 26.4px;
      white-space: pre;
      height:50%;
      width:48%;
      border:1px solid #ccc;
      overflow:auto;
    }
    .checkbox {
      font-size: 30px;
      border: 2px;
    }
    .device {
      font-size: 15px;
    }
    .stream{
      font-size: 15px;
    }
    .fetch{
    }
    .wake{
    }
  </style>
  <script type="text/javascript">
    draw_chart = function() {
      var chart = c3.generate({
        data: {
          x: 'time',
          columns: [
              ['time',                   $times],
              ['buffer_level',           $buffer_levels],
          ],
          type: 'bar',
          types: {
              buffer_level: 'line',
          },
        },
        zoom: {
          enabled: true,
        },

        grid: {
          x: {
            lines: [
              $grids,
            ],
          },
        },

        axis: {
          y: {min: 0, max: $max_y},
        },
      });
    };

    logs = `$logs`;
    put_logs = function () {
      document.getElementById('logs').innerHTML = logs;
    };

    set_initial_checkbox_value = function () {
      document.getElementById('device').checked = true;
      document.getElementById('stream').checked = true;
      document.getElementById('fetch').checked = true;
      document.getElementById('wake').checked = true;
    }

    window.onload = function() {
      draw_chart();
      put_logs();
      set_initial_checkbox_value();
    };

    function handleClick(checkbox) {
      var class_name = checkbox.id;
      var elements = document.getElementsByClassName(class_name);
      var i;

      if (checkbox.checked) {
        display_value = "block";
      } else {
        display_value = "none"
      }

      console.log("change " + class_name + " to " + display_value);
      for (i = 0; i < elements.length; i++) {
        elements[i].style.display = display_value;
      }
    }

  </script>
</head>

<body>
  <div id="chart" style="height:50%; width:100%" ></div>
  <div style="margin:0 auto"; class="checkbox">
      <label><input type="checkbox" onclick="handleClick(this);" id="device">Show device removed/added event</label>
      <label><input type="checkbox" onclick="handleClick(this);" id="stream">Show stream removed/added event</label>
      <label><input type="checkbox" onclick="handleClick(this);" id="fetch">Show fetch event</label>
      <label><input type="checkbox" onclick="handleClick(this);" id="wake">Show wake by num_fds=1 event</label>
  </div>
  <div class="event_log_box", id="logs", style="float:left;"></div>
  <textarea class="event_log_box", id="text", style="float:right;"></textarea>
</body>
</html>
""")


Tag = collections.namedtuple('Tag', {'time', 'text', 'position', 'class_name'})
"""
The tuple for tags shown on the plot on certain time.
text is the tag to show, position is the tag position, which is one of
'start', 'middle', 'end', class_name is one of 'device', 'stream', 'fetch',
and 'wake' which will be their CSS class name.
"""

class EventData(object):
    """The base class of an event."""
    def __init__(self, time, name):
        """Initializes an EventData.

        @param time: A string for event time.
        @param name: A string for event name.

        """
        self.time = time
        self.name = name
        self._text = None
        self._position = None
        self._class_name = None

    def GetTag(self):
        """Gets the tag for this event.

        @returns: A Tag object. Returns None if no need to show tag.

        """
        if self._text:
            return Tag(
                    time=self.time, text=self._text, position=self._position,
                    class_name=self._class_name)
        return None


class DeviceEvent(EventData):
    """Class for device event."""
    def __init__(self, time, name, device):
        """Initializes a DeviceEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param device: A string for device index.

        """
        super(DeviceEvent, self).__init__(time, name)
        self.device = device
        self._position = 'start'
        self._class_name = 'device'


class DeviceRemovedEvent(DeviceEvent):
    """Class for device removed event."""
    def __init__(self, time, name, device):
        """Initializes a DeviceRemovedEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param device: A string for device index.

        """
        super(DeviceRemovedEvent, self).__init__(time, name, device)
        self._text = 'Removed Device %s' % self.device


class DeviceAddedEvent(DeviceEvent):
    """Class for device added event."""
    def __init__(self, time, name, device):
        """Initializes a DeviceAddedEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param device: A string for device index.

        """
        super(DeviceAddedEvent, self).__init__(time, name, device)
        self._text = 'Added Device %s' % self.device


class LevelEvent(DeviceEvent):
    """Class for device event with buffer level."""
    def __init__(self, time, name, device, level):
        """Initializes a LevelEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param device: A string for device index.
        @param level: An int for buffer level.

        """
        super(LevelEvent, self).__init__(time, name, device)
        self.level = level


class StreamEvent(EventData):
    """Class for event with stream."""
    def __init__(self, time, name, stream):
        """Initializes a StreamEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param stream: A string for stream id.

        """
        super(StreamEvent, self).__init__(time, name)
        self.stream = stream
        self._class_name = 'stream'


class FetchStreamEvent(StreamEvent):
    """Class for stream fetch event."""
    def __init__(self, time, name, stream):
        """Initializes a FetchStreamEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param stream: A string for stream id.

        """
        super(FetchStreamEvent, self).__init__(time, name, stream)
        self._text = 'Fetch %s' % self.stream
        self._position = 'end'
        self._class_name = 'fetch'


class StreamAddedEvent(StreamEvent):
    """Class for stream added event."""
    def __init__(self, time, name, stream):
        """Initializes a StreamAddedEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param stream: A string for stream id.

        """
        super(StreamAddedEvent, self).__init__(time, name, stream)
        self._text = 'Add stream %s' % self.stream
        self._position = 'middle'


class StreamRemovedEvent(StreamEvent):
    """Class for stream removed event."""
    def __init__(self, time, name, stream):
        """Initializes a StreamRemovedEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param stream: A string for stream id.

        """
        super(StreamRemovedEvent, self).__init__(time, name, stream)
        self._text = 'Remove stream %s' % self.stream
        self._position = 'middle'


class WakeEvent(EventData):
    """Class for wake event."""
    def __init__(self, time, name, num_fds):
        """Initializes a WakeEvent.

        @param time: A string for event time.
        @param name: A string for event name.
        @param num_fds: A string for number of fd that wakes audio thread up.

        """
        super(WakeEvent, self).__init__(time, name)
        self._position = 'middle'
        self._class_name = 'wake'
        if num_fds != '0':
            self._text = 'num_fds %s' % num_fds


class C3LogWriter(object):
    """Class to handle event data and fill an HTML page using c3.js library"""
    def __init__(self):
        """Initializes a C3LogWriter."""
        self.times = []
        self.buffer_levels = []
        self.tags = []
        self.max_y = 0

    def AddEvent(self, event):
        """Digests an event.

        Add a tag if this event needs to be shown on grid.
        Add a buffer level data into buffer_levels if this event has buffer
        level.

        @param event: An EventData object.

        """
        tag = event.GetTag()
        if tag:
            self.tags.append(tag)

        if isinstance(event, LevelEvent):
            self.times.append(event.time)
            self.buffer_levels.append(str(event.level))
            if event.level > self.max_y:
                self.max_y = event.level
            logging.debug('add data for a level event %s: %s',
                          event.time, event.level)

        if (isinstance(event, DeviceAddedEvent) or
            isinstance(event, DeviceRemovedEvent)):
            self.times.append(event.time)
            self.buffer_levels.append('null')

    def _GetGrids(self):
        """Gets the content to be filled for grids.

        @returns: A str for grid with format:
           '{value: time1, text: "tag1", position: "position1"},
            {value: time1, text: "tag1"},...'

        """
        grids = []
        for tag in self.tags:
            content = ('{value: %s, text: "%s", position: "%s", '
                       'class: "%s"}') % (
                              tag.time, tag.text, tag.position, tag.class_name)
            grids.append(content)
        grids_joined = ', '.join(grids)
        return grids_joined

    def FillPage(self, page_template):
        """Fills in the page template with content.

        @param page_template: A string for HTML page content with variables
                              to be filled.

        @returns: A string for filled page.

        """
        times = ', '.join(self.times)
        buffer_levels = ', '.join(self.buffer_levels)
        grids = self._GetGrids()
        filled = page_template.safe_substitute(
                times=times,
                buffer_levels=buffer_levels,
                grids=grids,
                max_y=str(self.max_y))
        return filled


class EventLogParser(object):
    """Class for event log parser."""
    def __init__(self):
        """Initializes an EventLogParse."""
        self.parsed_events = []

    def AddEventLog(self, event_log):
        """Digests a line of event log.

        @param event_log: A line for event log.

        """
        event = self._ParseOneLine(event_log)
        if event:
            self.parsed_events.append(event)

    def GetParsedEvents(self):
        """Gets the list of parsed events.

        @returns: A list of parsed EventData.

        """
        return self.parsed_events

    def _ParseOneLine(self, line):
        """Parses one line of event log.

        Split a line like
        169536.504763588  WRITE_STREAMS_FETCH_STREAM     id:0 cbth:512 delay:1136
        into time, name, and props where
        time = '169536.504763588'
        name = 'WRITE_STREAMS_FETCH_STREAM'
        props = {
            'id': 0,
            'cb_th': 512,
            'delay': 1136
        }

        @param line: A line of event log.

        @returns: A EventData object.

        """
        line_split = line.split()
        time, name = line_split[0], line_split[1]
        logging.debug('time: %s, name: %s', time, name)
        props = {}
        for index in xrange(2, len(line_split)):
            key, value = line_split[index].split(':')
            props[key] = value
        logging.debug('props: %s', props)
        return self._CreateEventData(time, name, props)

    def _CreateEventData(self, time, name, props):
        """Creates an EventData based on event name.

        @param time: A string for event time.
        @param name: A string for event name.
        @param props: A dict for event properties.

        @returns: A EventData object.

        """
        if name == 'WRITE_STREAMS_FETCH_STREAM':
            return FetchStreamEvent(time, name, stream=props['id'])
        if name == 'STREAM_ADDED':
            return StreamAddedEvent(time, name, stream=props['id'])
        if name == 'STREAM_REMOVED':
            return StreamRemovedEvent(time, name, stream=props['id'])
        if name in ['FILL_AUDIO', 'SET_DEV_WAKE']:
            return LevelEvent(
                    time, name, device=props['dev'],
                    level=int(props['hw_level']))
        if name == 'DEV_ADDED':
            return DeviceAddedEvent(time, name, device=props['dev'])
        if name == 'DEV_REMOVED':
            return DeviceRemovedEvent(time, name, device=props['dev'])
        if name == 'WAKE':
            return WakeEvent(time, name, num_fds=props['num_fds'])
        return None


class AudioThreadLogParser(object):
    """Class for audio thread log parser."""
    def __init__(self, path):
        """Initializes an AudioThreadLogParser.

        @param path: The audio thread log file path.

        """
        self.path = path
        self.content = None

    def Parse(self):
        """Prases the audio thread logs.

        @returns: A list of event log lines.

        """
        logging.debug('Using file: %s', self.path)
        with open(self.path, 'r') as f:
            self.content = f.read().splitlines()

        # Event logs starting at two lines after 'Audio Thread Event Log'.
        index_start = self.content.index('Audio Thread Event Log:') + 2
        # If input is from audio_diagnostic result, use aplay -l line to find
        # the end of audio thread event logs.
        try:
            index_end = self.content.index('=== aplay -l ===')
        except ValueError:
            logging.debug(
                    'Can not find aplay line. This is not from diagnostic')
            index_end = len(self.content)
        event_logs = self.content[index_start:index_end]
        logging.info('Parsed %s log events', len(event_logs))
        return event_logs

    def FillLogs(self, page_template):
        """Fills the HTML page template with contents for audio thread logs.

        @param page_template: A string for HTML page content with log variable
                              to be filled.

        @returns: A string for filled page.

        """
        logs = '\n<br>'.join(self.content)
        return page_template.substitute(logs=logs)


def ParseArgs():
    """Parses the arguments.

    @returns: The namespace containing parsed arguments.

    """
    parser = argparse.ArgumentParser(
            description='Draw time chart from audio thread log',
            formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('FILE', type=str, help='The audio thread log file')
    parser.add_argument('-o', type=str, dest='output',
                        default='view.html', help='The output HTML file')
    parser.add_argument('-d', dest='debug', action='store_true',
                        default=False, help='Show debug message')
    return parser.parse_args()


def Main():
    """The Main program."""
    options = ParseArgs()
    logging.basicConfig(
            format='%(asctime)s:%(levelname)s:%(message)s',
            level=logging.DEBUG if options.debug else logging.INFO)

    # Gets lines of event logs.
    audio_thread_log_parser = AudioThreadLogParser(options.FILE)
    event_logs = audio_thread_log_parser.Parse()

    # Parses event logs into events.
    event_log_parser = EventLogParser()
    for event_log in event_logs:
        event_log_parser.AddEventLog(event_log)
    events = event_log_parser.GetParsedEvents()

    # Reads in events in preparation of filling HTML template.
    c3_writer = C3LogWriter()
    for event in events:
        c3_writer.AddEvent(event)

    # Fills in buffer level chart.
    page_content_with_chart = c3_writer.FillPage(page_content)

    # Fills in audio thread log into text box.
    page_content_with_chart_and_logs = audio_thread_log_parser.FillLogs(
            string.Template(page_content_with_chart))

    with open(options.output, 'w') as f:
        f.write(page_content_with_chart_and_logs)


if __name__ == '__main__':
    Main()