from app import app
from util.streamlayerformat import StreamLayerMerger
from formats.tarimageformatter import TarImageFormatter

import json
import re

class ACIImage(TarImageFormatter):
  """ Image formatter which produces an ACI-compatible TAR.
  """

  def stream_generator(self, namespace, repository, tag, synthetic_image_id,
                       layer_json, get_image_iterator, get_layer_iterator):
    # ACI Format (.tar):
    #   manifest - The JSON manifest
    #   rootfs - The root file system

    # Yield the manifest.
    yield self.tar_file('manifest', self._build_manifest(namespace, repository, tag, layer_json,
                                                         synthetic_image_id))

    # Yield the merged layer dtaa.
    yield self.tar_folder('rootfs')

    layer_merger = StreamLayerMerger(get_layer_iterator, path_prefix='rootfs/')
    for entry in layer_merger.get_generator():
      yield entry

  @staticmethod
  def _build_isolators(docker_config):
    """ Builds ACI isolator config from the docker config. """

    def _isolate_memory(memory):
      return {
        "name": "memory/limit",
        "value": str(memory) + 'B'
      }

    def _isolate_swap(memory):
      return {
        "name": "memory/swap",
        "value": str(memory) + 'B'
      }

    def _isolate_cpu(cpu):
      return {
        "name": "cpu/shares",
        "value": str(cpu)
      }

    def _isolate_capabilities(capabilities_set_value):
      capabilities_set = re.split(r'[\s,]', capabilities_set_value)
      return {
        "name": "capabilities/bounding-set",
        "value": ' '.join(capabilities_set)
      }

    mappers = {
      'Memory': _isolate_memory,
      'MemorySwap': _isolate_swap,
      'CpuShares': _isolate_cpu,
      'Cpuset': _isolate_capabilities
    }

    isolators = []

    for config_key in mappers:
      value = docker_config.get(config_key)
      if value:
        isolators.append(mappers[config_key](value))

    return isolators

  @staticmethod
  def _build_ports(docker_config):
    """ Builds the ports definitions for the ACI. """
    ports = []

    for docker_port_definition in docker_config.get('ports', {}):
      # Formats:
      # port/tcp
      # port/udp
      # port

      protocol = 'tcp'
      port_number = -1

      if '/' in docker_port_definition:
        (port_number, protocol) = docker_port_definition.split('/')
      else:
        port_number = docker_port_definition

      try:
        port_number = int(port_number)
        ports.append({
          "name": "port-%s" % port_number,
          "port": port_number,
          "protocol": protocol
        })
      except ValueError:
        pass

    return ports

  @staticmethod
  def _build_volumes(docker_config):
    """ Builds the volumes definitions for the ACI. """
    volumes = []
    names = set()

    def get_name(docker_volume_path):
      parts = docker_volume_path.split('/')
      name = ''

      while True:
        name = name + parts[-1]
        parts = parts[0:-1]
        if names.add(name):
          break

        name = '/' + name

      return name

    for docker_volume_path in docker_config.get('volumes', {}):
      volumes.append({
        "name": get_name(docker_volume_path),
        "path": docker_volume_path,
        "readOnly": False
      })
    return volumes


  @staticmethod
  def _build_manifest(namespace, repository, tag, docker_layer_data, synthetic_image_id):
    """ Builds an ACI manifest from the docker layer data. """

    config = docker_layer_data.get('config', {})

    source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
                                       app.config['SERVER_HOSTNAME'],
                                       namespace, repository, tag)

    # ACI requires that the execution command be absolutely referenced. Therefore, if we find
    # a relative command, we give it as an argument to /bin/sh to resolve and execute for us.
    entrypoint = config.get('Entrypoint', []) or []
    exec_path = entrypoint + (config.get('Cmd', []) or [])
    if exec_path and not exec_path[0].startswith('/'):
      exec_path = ['/bin/sh', '-c', '""%s""' % ' '.join(exec_path)]

    # TODO(jschorr): ACI doesn't support : in the name, so remove any ports.
    hostname = app.config['SERVER_HOSTNAME']
    hostname = hostname.split(':', 1)[0]

    manifest = {
      "acKind": "ImageManifest",
      "acVersion": "0.2.0",
      "name": '%s/%s/%s/%s' % (hostname, namespace, repository, tag),
      "labels": [
          {
              "name": "version",
              "value": "1.0.0"
          },
          {
              "name": "arch",
              "value": docker_layer_data.get('architecture', 'amd64')
          },
          {
              "name": "os",
              "value": docker_layer_data.get('os', 'linux')
          }
      ],
      "app": {
        "exec": exec_path,
        # Below, `or 'root'` is required to replace empty string from Dockerfiles.
        "user": config.get('User', '') or 'root',
        "group": config.get('Group', '') or 'root',
        "eventHandlers": [],
        "workingDirectory": config.get('WorkingDir', '') or '/',
        "environment": [{"name": key, "value": value}
                        for (key, value) in [e.split('=') for e in config.get('Env', []) or []]],
        "isolators": ACIImage._build_isolators(config),
        "mountPoints": ACIImage._build_volumes(config),
        "ports": ACIImage._build_ports(config),
        "annotations": [
          {"name": "created", "value": docker_layer_data.get('created', '')},
          {"name": "homepage", "value": source_url},
          {"name": "quay.io/derived-image", "value": synthetic_image_id},
        ]
      },
    }

    return json.dumps(manifest)