import weakref
from collections import namedtuple
from numbers import Real

from clubsandwich.geom import Point, Rect, Size
from clubsandwich.blt.context import BearLibTerminalContext
from .layout_options import LayoutOptions


ZERO_RECT = Rect(Point(0, 0), Size(0, 0)) 


def _option_field_to_id(val):
    if val == 'frame':
      value_start = 'frame'
    elif isinstance(val, Real):
      value_start = 'fraction'
    else:
      value_start = 'derive'


class View:
  """
  :param Rect frame: Rect relative to superview's :py:attr:`View.bounds`
  :param list subviews: List of subviews
  :param UIScene scene: Scene that's handling this view
  :param LayoutOptions layout_options: How to position this view
  :param bool clear: If ``True``, clear bounds each render. (Each individual
                     view implements this because it depends on color.)

  Renders itself and its subviews in an area of 2D space relative to its
  superview.

  :ivar list subviews: List of subviews
  :ivar bool is_hidden: ``True`` iff this view will not be drawn. Feel free to
                        set this yourself.
  :ivar LayoutOptions layout_options:
  :ivar Rect layout_spec: A copy of the frame made at init time, used by
                         :py:class:`LayoutOptions` to derive values during
                         layout.
  :ivar bool is_first_responder: ``True`` iff this view is currently the first
                                 responder.
  """

  def __init__(self, frame=None, subviews=None, scene=None, layout_options=None, clear=False):
    if isinstance(layout_options, dict):  # have pity on the user's imports
      opts = LayoutOptions()._asdict()
      opts.update(layout_options)
      layout_options = LayoutOptions(**opts)
    self.clear = clear
    self._scene = scene
    self._superview_weakref = lambda: None
    self.needs_layout = True
    self._frame = frame or ZERO_RECT
    self._bounds = self.frame.with_origin(Point(0, 0))
    self.subviews = []
    self.add_subviews(subviews or [])
    self.is_first_responder = False
    self.is_hidden = False

    self.layout_spec = frame
    self.layout_options = layout_options or LayoutOptions()

  ### core api ###

  @property
  def scene(self):
    """
    The scene this view is being rendered in, or ``None``.
    """
    if self._scene:
      return self._scene
    else:
      return self.superview.scene

  @property
  def superview(self):
    """
    Weak reference to the view this view is a child of, or ``None``.
    """
    try:
      return self._superview_weakref()
    except AttributeError:
      return None  # it may be super early in init time

  @superview.setter
  def superview(self, new_value):
    if new_value:
      self._superview_weakref = weakref.ref(new_value)
    else:
      self._superview_weakref = lambda: None

  def set_needs_layout(self, val=True):
    """
    :param bool val: If ``True``, view needs to be redrawn. (default ``True``)

    Call this if the view's :py:meth:`~clubsandwich.ui.View.frame` or content
    changes. :py:meth:`~View.draw` is only called if this was called first.

    Note that if you're changing either :py:attr:`View.layout_options` or
    changing something that affects the view's springs-and-struts layout
    metrics, you may need to call ``self.superview.set_needs_layout()`` to
    have the layout algorithm re-run on your view.
    """
    self.needs_layout = val

  def add_subviews(self, subviews):
    """
    :param list subviews: List of :py:class:`View` objects

    Append to this view's subviews
    """
    for v in subviews:
      v.superview = self
    self.subviews.extend(subviews)

  def remove_subviews(self, subviews):
    """
    :param list subviews: List of :py:class:`View` objects

    Remove the given subviews from this view
    """
    for v in subviews:
      v.superview = None
    self.subviews = [v for v in self.subviews if v not in subviews]

  def add_subview(self, subview):
    self.add_subviews([subview])

  def remove_subview(self, subview):
    self.remove_subviews([subview])

  def perform_draw(self, ctx):
    """
    Internal. Recursively draw all dirty views. Do not call or subclass this
    unless you are avoiding :py:class:`UIScene` for some reason.
    """
    if self.is_hidden:
      return
    self.draw(ctx)
    for view in self.subviews:
      with ctx.translate(view.frame.origin):
        view.perform_draw(ctx)

  def draw(self, ctx):
    """
    :param BearLibTerminalContext ctx:

    Draw this view. *ctx* is a full copy of the BearLibTerminal API moved into
    this view's frame of reference, so you can use (0, 0) as the upper left
    corner.

    This method will not be called if :py:attr:`View.is_hidden` is ``True``.
    """
    if self.clear:
      ctx.clear_area(self.bounds)

  def perform_layout(self):
    """
    Internal. Recursively layout all dirty views. Do not call or subclass this
    unless you are avoiding :py:class:`UIScene` for some reason.
    """
    if self.needs_layout:
      self.layout_subviews()
      self.needs_layout = False
    for view in self.subviews:
      view.perform_layout()

  def layout_subviews(self):
    """
    Set the frames of all subviews relative to ``self.bounds``. By default,
    applies the springs-and-struts algorithm using each view's
    ``layout_options`` and ``layout_spec`` properties.

    You shouldn't need to override this unless :py:class:`LayoutOptions` isn't
    expressive enough for you.
    """
    for view in self.subviews:
      view.apply_springs_and_struts_layout_in_superview()

  ### bounds, frame ###

  @property
  def intrinsic_size(self):
    """
    Optional. Values for ``intrinsic``-valued attributes of
    :py:attr:`LayoutOptions`.
    """
    raise NotImplementedError()

  @property
  def frame(self):
    """
    This view's rect *relative to its superview's bounds*.
    """
    return self._frame

  @frame.setter
  def frame(self, new_value):
    if new_value == self._frame:
      return
    self._frame = new_value
    self._bounds = new_value.with_origin(Point(0, 0))
    self.set_needs_layout(True)

  @property
  def bounds(self):
    """
    This view's rect from its internal frame of reference. That means
    ``self.bounds.origin`` is always ``Point(0, 0)``.
    """
    return self._bounds

  @bounds.setter
  def bounds(self, new_value):
    if new_value.origin != Point(0, 0):
        raise ValueError("Bounds is always anchored at (0, 0)")
    if new_value == self._bounds:
      return
    self._bounds = new_value
    self._frame = self._frame.with_size(new_value.size)
    self.set_needs_layout(True)

  ### responder chain, input ###

  @property
  def can_become_first_responder(self):
    """
    View subclasses should return ``True`` iff they want to be selectable and
    handle user input.
    """
    return False

  @property
  def contains_first_responders(self):
    return False

  @property
  def can_resign_first_responder(self):
    """
    View subclass can return ``True`` to prevent the ``tab`` key from taking
    focus away. It should be rare to need this.
    """
    return True

  def did_become_first_responder(self):
    """
    Called immediately after view becomes the first responder.
    """
    self.set_needs_layout(True)
    self.is_first_responder = True

  def did_resign_first_responder(self):
    """
    Called immediately after view resigns first responder status.
    """
    self.set_needs_layout(True)
    self.is_first_responder = False

  def descendant_did_become_first_responder(self, view):
    """
    :param View view:

    Called when any descendant of this view becomes the first responder. This
    is so scrollable view containers can scroll it into view.
    """
    pass

  def descendant_did_resign_first_responder(self, view):
    """
    :param View view:

    Called when any descendant of this view unbecomes the first responder.
    This is so scrollable view containers can release keyboard event handlers.
    """
    pass

  def terminal_read(self, val):
    """
    :param val: Return value of ``terminal_read()``
    :return: bool (``True`` if you handled the event)

    Fires when an input event occurs, and either:

    * This view is the first responder
    * The first responder is a descendant, and no other descendants have
      already handled this event

    You **must** return a truthy value if you handled the event so it doesn't get
    handled twice.
    """
    return False

  @property
  def first_responder_container_view(self):
    """
    The ancestor (including ``self``) that is a
    :py:class:`FirstResponderContainerView`.

    The most common use for this will probably be to manually change the
    first responder::

      def a_method_on_your_view(self):
        # forceably become first responder, muahaha!
        self.first_responder_container_view.set_first_responder(self)
    """
    # pretty hacky way to check for this but whatever
    if hasattr(self, 'first_responder'):
      return self
    for v in self.ancestors:
      if hasattr(v, 'first_responder'):
        return v
    return None

  ### tree traversal ###

  @property
  def leftmost_leaf(self):
    """
    Leftmost leaf of the tree.
    """
    if self.subviews:
      return self.subviews[0].leftmost_leaf
    else:
      return self

  @property
  def postorder_traversal(self):
    """
    Generator of all nodes in this subtree, including ``self``, such that a
    view is visited after all its subviews.
    """
    for v in self.subviews:
      yield from v.postorder_traversal
    yield self

  @property
  def ancestors(self):
    """
    Generator of all ancestors of this view, not including ``self``.
    """
    v = self.superview
    while v:
      yield v
      v = v.superview

  def get_ancestor_matching(self, predicate):
    """
    :param func predicate: ``predicate(View) -> bool``

    Returns the ancestor matching the given predicate, or ``None``.
    """
    for ancestor in self.ancestors:
      if predicate(v):
        return v
    return None

  @property
  def debug_string(self):
    """A string containing helpful debugging info for this view"""
    return '{} {!r}'.format(type(self).__name__, self.frame)

  def debug_print(self, indent=0):
    """Print hierarchical representation of this view and its subviews to
    stdout"""
    print(' ' * indent + self.debug_string)
    for sv in self.subviews:
      sv.debug_print(indent + 2)

  def __str__(self):
    return self.debug_string

  def __repr__(self):
    return self.debug_string

  def apply_springs_and_struts_layout_in_superview(self):
    options = self.layout_options
    spec = self.layout_spec
    superview_bounds = self.superview.bounds

    fields = [
      ('left', 'right', 'x', 'width'),
      ('top', 'bottom', 'y', 'height'),
    ]

    final_frame = Rect(Point(-1000, -1000), Size(-1000, -1000))

    for field_start, field_end, field_coord, field_size in fields:
      debug_string = options.get_debug_string_for_keys(
          [field_start, field_size, field_end])
      matches = (
        options.get_is_defined(field_start),
        options.get_is_defined(field_size),
        options.get_is_defined(field_end))
      if matches == (True, True, True):
        raise ValueError("Invalid spring/strut definition: {}".format(debug_string))
      if matches == (False, False, False):
        raise ValueError("Invalid spring/strut definition: {}".format(debug_string))
      elif matches == (True, False, False):
        setattr(
          final_frame, field_coord,
          options.get_value(field_start, self))
        # pretend that size is constant from frame
        setattr(
          final_frame, field_size,
          getattr(spec, field_size))
      elif matches == (True, True, False):
        setattr(
          final_frame, field_coord,
          options.get_value(field_start, self))
        setattr(
          final_frame, field_size,
          options.get_value(field_size, self))
      elif matches == (False, True, False):  # magical centering!
        size_val = options.get_value(field_size, self)
        setattr(final_frame, field_size, size_val)
        setattr(
          final_frame, field_coord,
          getattr(superview_bounds, field_size) / 2 - size_val / 2)
      elif matches == (False, True, True):
        size_val = options.get_value(field_size, self)
        setattr(
          final_frame, field_coord,
          getattr(superview_bounds, field_size) - options.get_value(field_end, self) - size_val)
        setattr(final_frame, field_size, size_val)
      elif matches == (False, False, True):
        setattr(
          final_frame, field_coord,
          getattr(superview_bounds, field_size) - options.get_value(field_end, self))
        # pretend that size is constant from frame
        setattr(final_frame, field_size, getattr(spec, field_size))
      elif matches == (True, False, True):
        start_val = options.get_value(field_start, self) 
        end_val = options.get_value(field_end, self) 
        setattr(
          final_frame, field_coord, start_val)
        setattr(
          final_frame, field_size,
          getattr(superview_bounds, field_size) - start_val - end_val)
      else:
        raise ValueError("Unhandled case: {}".format(debug_string))

    assert(final_frame.x != -1000)
    assert(final_frame.y != -1000)
    assert(final_frame.width != -1000)
    assert(final_frame.height != -1000)
    self.frame = final_frame.floored
