# 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.
# ==============================================================================
"""Configurations for TensorFlow Debugger (TFDBG) command-line interfaces."""
import collections
import json
import os

from tensorflow.python.debug.cli import debugger_cli_common
from tensorflow.python.platform import gfile

RL = debugger_cli_common.RichLine


class CLIConfig(object):
  """Client-facing configurations for TFDBG command-line interfaces."""

  _CONFIG_FILE_NAME = ".tfdbg_config"

  _DEFAULT_CONFIG = [
      ("graph_recursion_depth", 20),
      ("mouse_mode", True),
  ]

  def __init__(self, config_file_path=None):
    self._config_file_path = (config_file_path or
                              self._default_config_file_path())
    self._config = collections.OrderedDict(self._DEFAULT_CONFIG)
    if gfile.Exists(self._config_file_path):
      config = self._load_from_file()
      for key, value in config.items():
        self._config[key] = value
    self._save_to_file()

    self._set_callbacks = {}

  def get(self, property_name):
    if property_name not in self._config:
      raise KeyError("%s is not a valid property name." % property_name)
    return self._config[property_name]

  def set(self, property_name, property_val):
    """Set the value of a property.

    Supports limitd property value types: `bool`, `int` and `str`.

    Args:
      property_name: Name of the property.
      property_val: Value of the property. If the property has `bool` type and
        this argument has `str` type, the `str` value will be parsed as a `bool`

    Raises:
      ValueError: if a `str` property_value fails to be parsed as a `bool`.
      KeyError: if `property_name` is an invalid property name.
    """
    if property_name not in self._config:
      raise KeyError("%s is not a valid property name." % property_name)

    orig_val = self._config[property_name]
    if isinstance(orig_val, bool):
      if isinstance(property_val, str):
        if property_val.lower() in ("1", "true", "t", "yes", "y", "on"):
          property_val = True
        elif property_val.lower() in ("0", "false", "f", "no", "n", "off"):
          property_val = False
        else:
          raise ValueError(
              "Invalid string value for bool type: %s" % property_val)
      else:
        property_val = bool(property_val)
    elif isinstance(orig_val, int):
      property_val = int(property_val)
    elif isinstance(orig_val, str):
      property_val = str(property_val)
    else:
      raise TypeError("Unsupported property type: %s" % type(orig_val))
    self._config[property_name] = property_val
    self._save_to_file()

    # Invoke set-callback.
    if property_name in self._set_callbacks:
      self._set_callbacks[property_name](self._config)

  def set_callback(self, property_name, callback):
    """Set a set-callback for given property.

    Args:
      property_name: Name of the property.
      callback: The callback as a `callable` of signature:
          def cbk(config):
        where config is the config after it is set to the new value.
        The callback is invoked each time the set() method is called with the
        matching property_name.

    Raises:
      KeyError: If property_name does not exist.
      TypeError: If `callback` is not callable.
    """
    if property_name not in self._config:
      raise KeyError("%s is not a valid property name." % property_name)
    if not callable(callback):
      raise TypeError("The callback object provided is not callable.")
    self._set_callbacks[property_name] = callback

  def _default_config_file_path(self):
    return os.path.join(os.path.expanduser("~"), self._CONFIG_FILE_NAME)

  def _save_to_file(self):
    try:
      with gfile.Open(self._config_file_path, "w") as config_file:
        json.dump(self._config, config_file)
    except IOError:
      pass

  def summarize(self, highlight=None):
    """Get a text summary of the config.

    Args:
      highlight: A property name to highlight in the output.

    Returns:
      A `RichTextLines` output.
    """
    lines = [RL("Command-line configuration:", "bold"), RL("")]
    for name, val in self._config.items():
      highlight_attr = "bold" if name == highlight else None
      line = RL("  ")
      line += RL(name, ["underline", highlight_attr])
      line += RL(": ")
      line += RL(str(val), font_attr=highlight_attr)
      lines.append(line)
    return debugger_cli_common.rich_text_lines_from_rich_line_list(lines)

  def _load_from_file(self):
    try:
      with gfile.Open(self._config_file_path, "r") as config_file:
        config_dict = json.load(config_file)
        config = collections.OrderedDict()
        for key in sorted(config_dict.keys()):
          config[key] = config_dict[key]
        return config
    except (IOError, ValueError):
      # The reading of the config file may fail due to IO issues or file
      # corruption. We do not want tfdbg to error out just because of that.
      return dict()
