# Copyright 2017 The TensorFlow Authors. 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 __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import threading
import time

import numpy as np
import tensorflow as tf
from google.protobuf import message
from werkzeug import wrappers

from tensorboard import util
from tensorboard.backend import http_util
from tensorboard.backend.event_processing import plugin_asset_util as pau
from tensorboard.plugins import base_plugin
from tensorboard.plugins.beholder import file_system_tools
from tensorboard.plugins.beholder import im_util
from tensorboard.plugins.beholder import shared_config

DEFAULT_INFO = [{
    'name': 'Waiting for data...',
}]


class BeholderPlugin(base_plugin.TBPlugin):
  """
  TensorBoard plugin for viewing model data as a live video during training.
  """

  plugin_name = shared_config.PLUGIN_NAME

  def __init__(self, context):
    self._lock = threading.Lock()
    self._MULTIPLEXER = context.multiplexer
    self.PLUGIN_LOGDIR = pau.PluginDirectory(
        context.logdir, shared_config.PLUGIN_NAME)
    self.FPS = 10
    self._config_file_lock = threading.Lock()
    self.most_recent_frame = None
    self.most_recent_info = DEFAULT_INFO

  def get_plugin_apps(self):
    return {
        '/change-config': self._serve_change_config,
        '/beholder-frame': self._serve_beholder_frame,
        '/section-info': self._serve_section_info,
        '/ping': self._serve_ping,
        '/is-active': self._serve_is_active,
    }

  def is_active(self):
    summary_filename = '{}/{}'.format(
        self.PLUGIN_LOGDIR, shared_config.SUMMARY_FILENAME)
    info_filename = '{}/{}'.format(
        self.PLUGIN_LOGDIR, shared_config.SECTION_INFO_FILENAME)
    return tf.gfile.Exists(summary_filename) and\
           tf.gfile.Exists(info_filename)

  def is_config_writable(self):
    try:
      if not tf.gfile.Exists(self.PLUGIN_LOGDIR):
        tf.gfile.MakeDirs(self.PLUGIN_LOGDIR)
      config_filename = '{}/{}'.format(
          self.PLUGIN_LOGDIR, shared_config.CONFIG_FILENAME)
      with self._config_file_lock:
        file_system_tools.write_pickle(
            file_system_tools.read_pickle(
                config_filename, shared_config.DEFAULT_CONFIG),
            config_filename)
      return True
    except tf.errors.PermissionDeniedError as e:
      tf.logging.warning(
          'Unable to write Beholder config, controls will be disabled: %s', e)
      return False

  @wrappers.Request.application
  def _serve_is_active(self, request):
    is_active = self.is_active()
    # If the plugin isn't active, don't check if the configuration is writable
    # since that will leave traces on disk; instead return True (the default).
    is_config_writable = self.is_config_writable() if is_active else True
    response = {
        'is_active': is_active,
        'is_config_writable': is_config_writable,
    }
    return http_util.Respond(request, response, 'application/json')

  def _fetch_current_frame(self):
    path = '{}/{}'.format(self.PLUGIN_LOGDIR, shared_config.SUMMARY_FILENAME)
    with self._lock:
      try:
        frame = file_system_tools.read_tensor_summary(path).astype(np.uint8)
        self.most_recent_frame = frame
        return frame
      except (message.DecodeError, IOError, tf.errors.NotFoundError):
        if self.most_recent_frame is None:
          self.most_recent_frame = im_util.get_image_relative_to_script(
              'no-data.png')
        return self.most_recent_frame

  @wrappers.Request.application
  def _serve_change_config(self, request):
    config = {}

    for key, value in request.form.items():
      try:
        config[key] = int(value)
      except ValueError:
        if value == 'false':
          config[key] = False
        elif value == 'true':
          config[key] = True
        else:
          config[key] = value

    self.FPS = config['FPS']

    with self._config_file_lock:
      file_system_tools.write_pickle(
          config,
          '{}/{}'.format(self.PLUGIN_LOGDIR, shared_config.CONFIG_FILENAME))
    return http_util.Respond(request, {'config': config}, 'application/json')

  @wrappers.Request.application
  def _serve_section_info(self, request):
    path = '{}/{}'.format(
        self.PLUGIN_LOGDIR, shared_config.SECTION_INFO_FILENAME)
    with self._lock:
      default = self.most_recent_info
    info = file_system_tools.read_pickle(path, default=default)
    if info is not default:
      with self._lock:
        self.most_recent_info = info
    return http_util.Respond(request, info, 'application/json')

  def _frame_generator(self):
    while True:
      last_duration = 0

      if self.FPS == 0:
        continue
      else:
        time.sleep(max(0, 1/(self.FPS) - last_duration))

      start_time = time.time()
      array = self._fetch_current_frame()
      image_bytes = util.encode_png(array)

      frame_text = b'--frame\r\n'
      content_type = b'Content-Type: image/png\r\n\r\n'

      response_content = frame_text + content_type + image_bytes + b'\r\n\r\n'

      last_duration = time.time() - start_time
      yield response_content


  @wrappers.Request.application
  def _serve_beholder_frame(self, request): # pylint: disable=unused-argument
    # Thanks to Miguel Grinberg for this technique:
    # https://blog.miguelgrinberg.com/post/video-streaming-with-flask
    mimetype = 'multipart/x-mixed-replace; boundary=frame'
    return wrappers.Response(response=self._frame_generator(),
                             status=200,
                             mimetype=mimetype)

  @wrappers.Request.application
  def _serve_ping(self, request): # pylint: disable=unused-argument
    return http_util.Respond(request, {'status': 'alive'}, 'application/json')
