# -*- coding: utf-8 -*-
"""This module contains code for accessing values in nested data structures."""

__author__ = 'Jens Finkhaeuser'
__copyright__ = 'Copyright (c) 2018 Jens Finkhaeuser'
__license__ = 'MIT +no-false-attribs'
__all__ = ()


def path_get(obj, path, defaultvalue = None):
  """
  Retrieve the value from obj indicated by path.

  Like dict.get(), except:
  - Any Mapping or Sequence is supported.
  - Path is itself a Sequence; the first part is applied to the passed object,
    the second part to the value returned from this operation, and so forth
    recursively.

  :param mixed obj: The Sequence or Mapping from which to retrieve values.
  :param Sequence path: A Sequence of zero or more key/index elements.
  :param mixed defaultvalue: If the value at the path does not exist and this
    parameter is not None, it is returned. Otherwise an error is raised.
  """
  import collections

  if path is not None and not isinstance(path, collections.Sequence):
    raise TypeError('Path is a %s, but must be None or a Collection!'
            % (type(path),))

  if isinstance(obj, collections.Mapping):
    if path is None or len(path) < 1:
      return obj or defaultvalue
    return path_get(obj[path[0]], path[1:], defaultvalue)

  elif isinstance(obj, collections.Sequence):
    if path is None or len(path) < 1:
      return obj or defaultvalue

    if not isinstance(path[0], int):
      raise KeyError('Sequences need integer indices only.')

    return path_get(obj[path[0]], path[1:], defaultvalue)

  else:
    # Path must be empty.
    if path is not None and len(path) > 0:
      raise TypeError('Cannot get anything from type %s!' % (type(obj),))
    return obj or defaultvalue


def path_set(obj, path, value, **options):
  """
  Set the value in obj indicated by path.

  Setter anologous to path_get() above.

  As setting values is a write operation, this function optionally creates
  intermediate objects to ensure all elements of path can be dereferenced.

  :param mixed obj: The Sequence or Mapping from which to retrieve values.
  :param Sequence path: A Sequence of zero or more key/index elements.
  :param mixed value: The value to set.
  :param bool create: [optional] Flag indicating whether to create
    intermediate values or not. Defaults to False.
  """
  import collections

  # Retrieve options
  create = options.get('create', False)

  def fill_sequence(seq, index, value_index_type):
    """
    Fill the sequence seq with elements until index can be accessed.

    Fills with None except for the indexed element. That is either a dict or
    a list, depending on the value_index_type. If the latter is an int, a
    list is added. If the latter is None (unknown), None is added. Otherwise
    a dict is added.
    """
    if len(seq) > index:
      return

    while len(seq) < index:
      seq.append(None)

    if value_index_type == int:
      seq.append([])
    elif value_index_type is None:
      seq.append(None)
    else:
      seq.append({})

  def safe_idx(seq, index):
    """
    Safely index a sequence.

    Much like dict.get with default value, except returns None instead of
    raising IndexError.
    """
    try:
      return type(seq[index])
    except IndexError:
      return None

  # print('obj', obj, type(obj))
  # print('path', path)
  # print('value', value)

  if path is not None and not isinstance(path, collections.Sequence):
    raise TypeError('Path is a %s, but must be None or a Collection!'
            % (type(path),))

  if len(path) < 1:
    raise KeyError('Cannot set with an empty path!')

  if isinstance(obj, collections.Mapping):
    # If we don't have a mutable mapping, we should raise a TypeError
    if not isinstance(obj, collections.MutableMapping):  # pragma: nocover
      raise TypeError('Mapping is not mutable: %s' % (type(obj),))

    # If the path has only one element, we just overwrite the element at the
    # given key. Otherwise we recurse.
    if len(path) == 1:
      if not create and path[0] not in obj:
        # dicts would normally silently create, but we have to make it
        # explicit to fulfil our contract.
        raise KeyError('Key "%s" not in Mapping!' % (path[0],))
      obj[path[0]] = value
    else:
      if create and path[0] not in obj:
        if type(path[1]) == int:
          obj[path[0]] = []
        else:
          obj[path[0]] = {}
      path_set(obj[path[0]], path[1:], value, create = create)

    return obj

  elif isinstance(obj, collections.Sequence):
    # If we don't have a mutable sequence, we should raise a TypeError
    if not isinstance(obj, collections.MutableSequence):
      raise TypeError('Sequence is not mutable: %s' % (type(obj),))

    # Ensure integer indices
    if not isinstance(path[0], int):
      raise KeyError('Sequences need integer indices only.')

    # If we're supposed to create and the index at path[0] doesn't exist,
    # then we need to push some dummy objects.
    if create:
      fill_sequence(obj, path[0], safe_idx(path, 1))

    # If the path has only one element, we just overwrite the element at the
    # given index. Otherwise we recurse.
    # print('pl', len(path))
    if len(path) == 1:
      obj[path[0]] = value
    else:
      path_set(obj[path[0]], path[1:], value, create = create)

    return obj
  else:
    raise TypeError('Cannot set anything on type %s!' % (type(obj),))
