#!/usr/bin/env python
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Usage: build_super_image input_file output_dir_or_file

input_file: one of the following:
  - directory containing extracted target files. It will load info from
    META/misc_info.txt and build full super image / split images using source
    images from IMAGES/.
  - target files package. Same as above, but extracts the archive before
    building super image.
  - a dictionary file containing input arguments to build. Check
    `dump-super-image-info' for details.
    In addition:
    - If source images should be included in the output image (for super.img
      and super split images), a list of "*_image" should be paths of each
      source images.

output_dir_or_file:
    If a single super image is built (for super_empty.img, or super.img for
    launch devices), this argument is the output file.
    If a collection of split images are built (for retrofit devices), this
    argument is the output directory.
"""

from __future__ import print_function

import logging
import os.path
import shlex
import sys
import zipfile

import common
import sparse_img

if sys.hexversion < 0x02070000:
  print("Python 2.7 or newer is required.", file=sys.stderr)
  sys.exit(1)

logger = logging.getLogger(__name__)


UNZIP_PATTERN = ["IMAGES/*", "META/*"]


def GetArgumentsForImage(partition, group, image=None):
  image_size = sparse_img.GetImagePartitionSize(image) if image else 0

  cmd = ["--partition",
         "{}:readonly:{}:{}".format(partition, image_size, group)]
  if image:
    cmd += ["--image", "{}={}".format(partition, image)]

  return cmd


def BuildSuperImageFromDict(info_dict, output):

  cmd = [info_dict["lpmake"],
         "--metadata-size", "65536",
         "--super-name", info_dict["super_metadata_device"]]

  ab_update = info_dict.get("ab_update") == "true"
  retrofit = info_dict.get("dynamic_partition_retrofit") == "true"
  block_devices = shlex.split(info_dict.get("super_block_devices", "").strip())
  groups = shlex.split(info_dict.get("super_partition_groups", "").strip())

  if ab_update and retrofit:
    cmd += ["--metadata-slots", "2"]
  elif ab_update:
    cmd += ["--metadata-slots", "3"]
  else:
    cmd += ["--metadata-slots", "2"]

  if ab_update and retrofit:
    cmd.append("--auto-slot-suffixing")

  for device in block_devices:
    size = info_dict["super_{}_device_size".format(device)]
    cmd += ["--device", "{}:{}".format(device, size)]

  append_suffix = ab_update and not retrofit
  has_image = False
  for group in groups:
    group_size = info_dict["super_{}_group_size".format(group)]
    if append_suffix:
      cmd += ["--group", "{}_a:{}".format(group, group_size),
              "--group", "{}_b:{}".format(group, group_size)]
    else:
      cmd += ["--group", "{}:{}".format(group, group_size)]

    partition_list = shlex.split(
        info_dict["super_{}_partition_list".format(group)].strip())

    for partition in partition_list:
      image = info_dict.get("{}_image".format(partition))
      if image:
        has_image = True

      if not append_suffix:
        cmd += GetArgumentsForImage(partition, group, image)
        continue

      # For A/B devices, super partition always contains sub-partitions in
      # the _a slot, because this image should only be used for
      # bootstrapping / initializing the device. When flashing the image,
      # bootloader fastboot should always mark _a slot as bootable.
      cmd += GetArgumentsForImage(partition + "_a", group + "_a", image)

      other_image = None
      if partition == "system" and "system_other_image" in info_dict:
        other_image = info_dict["system_other_image"]
        has_image = True

      cmd += GetArgumentsForImage(partition + "_b", group + "_b", other_image)

  if info_dict.get("build_non_sparse_super_partition") != "true":
    cmd.append("--sparse")

  cmd += ["--output", output]

  common.RunAndCheckOutput(cmd)

  if retrofit and has_image:
    logger.info("Done writing images to directory %s", output)
  else:
    logger.info("Done writing image %s", output)

  return True


def BuildSuperImageFromExtractedTargetFiles(inp, out):
  info_dict = common.LoadInfoDict(inp)
  partition_list = shlex.split(
      info_dict.get("dynamic_partition_list", "").strip())

  if "system" in partition_list:
    image_path = os.path.join(inp, "IMAGES", "system_other.img")
    if os.path.isfile(image_path):
      info_dict["system_other_image"] = image_path

  missing_images = []
  for partition in partition_list:
    image_path = os.path.join(inp, "IMAGES", "{}.img".format(partition))
    if not os.path.isfile(image_path):
      missing_images.append(image_path)
    else:
      info_dict["{}_image".format(partition)] = image_path
  if missing_images:
    logger.warning("Skip building super image because the following "
                   "images are missing from target files:\n%s",
                   "\n".join(missing_images))
    return False
  return BuildSuperImageFromDict(info_dict, out)


def BuildSuperImageFromTargetFiles(inp, out):
  input_tmp = common.UnzipTemp(inp, UNZIP_PATTERN)
  return BuildSuperImageFromExtractedTargetFiles(input_tmp, out)


def BuildSuperImage(inp, out):

  if isinstance(inp, dict):
    logger.info("Building super image from info dict...")
    return BuildSuperImageFromDict(inp, out)

  if isinstance(inp, str):
    if os.path.isdir(inp):
      logger.info("Building super image from extracted target files...")
      return BuildSuperImageFromExtractedTargetFiles(inp, out)

    if zipfile.is_zipfile(inp):
      logger.info("Building super image from target files...")
      return BuildSuperImageFromTargetFiles(inp, out)

    if os.path.isfile(inp):
      with open(inp) as f:
        lines = f.read()
      logger.info("Building super image from info dict...")
      return BuildSuperImageFromDict(common.LoadDictionaryFromLines(lines.split("\n")), out)

  raise ValueError("{} is not a dictionary or a valid path".format(inp))


def main(argv):

  args = common.ParseOptions(argv, __doc__)

  if len(args) != 2:
    common.Usage(__doc__)
    sys.exit(1)

  common.InitLogging()

  BuildSuperImage(args[0], args[1])


if __name__ == "__main__":
  try:
    common.CloseInheritedPipes()
    main(sys.argv[1:])
  except common.ExternalError:
    logger.exception("\n   ERROR:\n")
    sys.exit(1)
  finally:
    common.Cleanup()
