import json

from .exceptions import InvalidInput


def describe(input_format):
  """
  Returns a JSON-able dictionary that describes the 'input_format' by relying on the '__name__' property of any
  functions
  """

  if input_format is None:
    return None
  if type(input_format) is dict:
    output = {}
    for key in input_format:
      output[key] = describe(input_format[key])
    return output
  if type(input_format) is list:
    output = [ describe(input_format[0]), '…' ]
    return output
  if callable(input_format):
    return input_format.__name__
  return str(input_format)


def with_extra_fields(input_format_dict):
  """
  Use this function to wrap a dictionary 'input_format' specification for use with the 'validate' function if you want
  all the properties of ths input data to come through, not just the ones we are explicitly validating.

  Consider the difference between the following two examples:
  validate({ "a": 1, "b": 2 }, { "a": int }) -> { "a": 1 }
  validate({ "a": 1, "b": 2 }, with_extra_fields({ "a": int })) -> { "a": 1, "b": 2 }

  Note that the wrapped input format is still evaluated:
  validate({ "a": "str" }, with_extra_fields({ "a": int })) -> InvalidInput
  """

  if type(input_format_dict) is not dict:
    raise Exception("Expected dictionary")
  def validate_with_extra_fields(input_data_dict):
    if type(input_data_dict) is not dict:
      raise Exception("Expected dictionary")
    output = {}
    output.update(input_data_dict)
    output.update(validate(input_data_dict, input_format_dict))
    return output
  return validate_with_extra_fields


def validate(input_data, input_format, _key=None):
  """
  Validates / parses the input_data against the input_format and returns a new object. Throws an InvalidInput exception
  if there is a problem with the input

  'input_format' can be a dictionary, a single-element list, or a single-argument callable that returns the parsed data.
  These objects can be nested. For lists, the input_format's first element (which must also be an input_format) will be
  applied to all elements in the input_data list. You can specify optional dictionary keys in the input format by
  enclosing them in square brackets. '_key' is internally used for recursion to report exactly what fields failed and
  should not be set.

  Examples:
  validate(None, None) -> None
  validate(5, int) -> 5
  validate("not_an_int", int) -> InvalidInput
  validate({ "n": 5 }, None) -> None
  validate({ "n": 5 }, { "n": int }) -> { "n": 5 } # Note that this is _not_ the original dictionary
  validate({ "n": "5" }, { "n": int }) -> { "n": 5 } # Note that 5 is now an int (because int(5) -> 5)
  validate({ "n": "not_an_int" }, { "n": int }) -> InvalidInput
  validate({ "a": 1 }, { "a": int, "[b]": int }) -> { "a": 1 } # The lack of the optional "b" parameter is ignored
  validate({ "a": 1, "b": 2 }, { "a": int, "[b]": int }) -> { "a": 1, "b": 2 }
  validate({ "a": 1, "b": "not_an_int" }, { "a": int, "[b]": int }) -> InvalidInput
  validate([ 1, "2" ], [ int ]) -> [ 1, 2 ]
  validate([ 1, "not_an_int" ], [ int ]) -> InvalidInput
  """

  if not input_format:
    return None

  try:

    if type(input_format) is dict:
      if type(input_data) is not dict:
        raise Exception('Expected dictionary')
      output = {}
      for key in input_format:
        if len(key) > 2 and key[0] == '[' and key[-1] == ']':
          optional_key = key[1:-1]
          if optional_key in input_data:
            output[optional_key] = validate(input_data[optional_key], input_format[key], _key=optional_key)
        else:
          output[key] = validate(input_data[key], input_format[key], _key=key)
      return output

    if type(input_format) is list:
      if type(input_data) is not list:
        raise Exception('Expected list')
      input_data_size = len(input_data)
      output = input_data_size * [None]
      for i in range(input_data_size):
        output[i] = validate(input_data[i], input_format[0], _key=("%s[%i]" % (_key or "", i)))
      return output

    if callable(input_format):
      return input_format(input_data)

    if (input_format == input_data):
      return input_data

    raise Exception()

  except InvalidInput:
    raise

  except Exception as e:
    e_cause_msg = e.__class__.__name__
    if str(e):
      e_cause_msg += ": " + str(e)
    key_description = ("Value for key '%s'" % _key) if _key else "Value"
    msg = "%s does not match the following input format '%s' - %s" % (key_description, describe(input_format), e_cause_msg)
    raise InvalidInput(msg)
