import math

__all__ = [
  'expand',
]

def get_value(key: str, values):
  tokens = key.split('.')
  value = values

  for token in tokens:
    if token.isdigit():
      index = int(token)
      try:
        value = value[index]
      except Exception as e:
        raise ValueError(f'{value} does not have key {token} (full key: {key})') from e

    else:
      if token in value:
        value = value[token]
      else:
        raise ValueError(f'{value} does not have key {token} (full key: {key})')

  return value

def expand_template(template, values):
  if isinstance(template, dict):
    concrete = {}

    for k, v in template.items():
      if k.startswith('$'):
        k_expanded = get_value(k[1:], values)
      else:
        k_expanded = k

      concrete[k_expanded] = expand_template(template[k], values)

    return concrete

  elif isinstance(template, list):
    return [expand_template(temp, values) for temp in template]

  elif isinstance(template, str):
    if template.startswith('$'):
      return get_value(template[1:], values)
    else:
      return template

  else:
    return template


def _expand(template, values, name):
  normalized_values = {}
  normalized_names = {}
  for arg, vs in values.items():
    if isinstance(vs, (tuple, list)):
      normalized_values[arg] = vs
      normalized_names[arg] = [str(i) for i, _ in enumerate(vs)]

    elif isinstance(vs, dict):
      normalized_names[arg], normalized_values[arg] = zip(*[
        (k, v) for k, v in vs.items()
      ])

    else:
      raise ValueError('values must be either list of dict')

  total = math.prod([len(vs) for _, vs in normalized_values.items()])
  expanded = {}

  for i in range(total):
    i_ = i
    instance_config = {}
    value_names = []

    for arg, vs in normalized_values.items():
      j = i_ % len(vs)
      i_ = i_ // len(vs)
      instance_config[arg] = vs[j]
      value_names.append(str(normalized_names[arg][j]))

    instance_postfix = '-'.join(value_names)

    if len(instance_postfix) > 0:
      instance_name = f'{name}-{instance_postfix}'
    else:
      instance_name = name

    ### may occur if there is nothing to expand
    if instance_name in expanded:
      raise ValueError(f'name collision {instance_name}, please, check template _postfix')

    expanded[instance_name] = expand_template(template, instance_config)

  return expanded

def expand(library):
  """
  Expands a library into concrete configurations.

  Iterates through the library. Entries with '_template' and '_values'
  are expanded into multiple configurations (one for each combination of values),
  while other entries are passed through unchanged.
  Returns the name of the library if any (specified by '_name') and expanded configurations.
  """
  expanded = {}

  for key, config in library.items():
    if '__template__' in config:
      template = config['__template__']
      values = config['__values__']

      for k, v in _expand(template, values, key).items():
        if k in expanded:
          raise ValueError(f'name collision during expansion {k}')
        else:
          expanded[k] = v
    else:
      expanded[key] = config

  return expanded