# Copyright 2015 The Bazel Authors. All rights reserved.
#
# 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.

"""Construct a dex manifest from a set of input .dex.zip files.

Usage: %s <output manifest> <input zip file>*
       %s @<params file>

Input files must be either .zip files containing one or more .dex files or
.dex files.

A manifest file is written that contains one line for each input dex in the
following form:

<input zip> <path in input zip> <path in output zip> <MD5 checksum>

or

<input dex> - <path in output zip> <SHA-256 checksum>
"""

import hashlib
import os
# pylint: disable=g-import-not-at-top
try:
  # python2 without compatibility package
  from Queue import Queue
except ImportError:
  # python3
  from queue import Queue
import shutil
import sys
import tempfile
from threading import Thread
import zipfile


class DexmanifestBuilder(object):
  """Implementation of the dex manifest builder."""

  def __init__(self):
    self.manifest_lines = []
    self.dir_counter = 1
    self.output_dex_counter = 1
    self.checksums = set()
    self.tmpdir = None
    self.queue = Queue()
    self.threads_list = list()

  def __enter__(self):
    self.tmpdir = tempfile.mkdtemp()
    return self

  def __exit__(self, unused_type, unused_value, unused_traceback):
    shutil.rmtree(self.tmpdir, True)

  def Checksum(self, filename, input_dex_or_zip, zippath):
    """Compute the SHA-256 checksum of a file.

    This method could be invoked concurrently.

    Therefore we need to include other metadata like input_dex_or_zip to
    keep the context.
    """
    h = hashlib.sha256()
    with open(filename, "rb") as f:
      while True:
        data = f.read(65536)
        if not data:
          break

        h.update(data)

    return h.hexdigest(), input_dex_or_zip, zippath

  def AddDexes(self, dex_metadata_list):
    """Adds all dex file together to the output.

    Sort the result to make sure the dexes order are always the same given
    the same input.
    Args:
      dex_metadata_list: A list of [fs_checksum, input_dex_or_zip, zippath],
        where fs_checksum is the SHA-256 checksum for dex file, input_dex_or_zip
        is the input file written to the manifest, zippath is the zip path
        written to the manifest or None if the input file is not a .zip.

    Returns:
      None.
    """
    dex_metadata_list_sorted = sorted(
        dex_metadata_list, key=lambda x: (x[1], x[2]))
    for dex_metadata in dex_metadata_list_sorted:
      fs_checksum, input_dex_or_zip, zippath = dex_metadata[0], dex_metadata[
          1], dex_metadata[2]
      if fs_checksum in self.checksums:
        return
      self.checksums.add(fs_checksum)
      zip_dex = "incremental_classes%d.dex" % self.output_dex_counter
      self.output_dex_counter += 1
      self.manifest_lines.append(
          "%s %s %s %s" %
          (input_dex_or_zip, zippath if zippath else "-", zip_dex, fs_checksum))

  def ComputeChecksumConcurrently(self, input_dex_or_zip, zippath, dex):
    """Call Checksum concurrently to improve build performance when an app contains multiple dex files."""
    t = Thread(target=lambda q, arg1, arg2, arg3: q.put(self.Checksum(arg1, arg2, arg3)), \
      args=(self.queue, dex, input_dex_or_zip, zippath))
    t.start()
    self.threads_list.append(t)

  def Run(self, argv):
    """Creates a dex manifest."""
    if len(argv) < 1:
      raise Exception("At least one argument expected")

    if argv[0][0] == "@":
      if len(argv) != 1:
        raise IOError("A parameter file should be the only argument")
      with open(argv[0][1:]) as param_file:
        argv = [a.strip() for a in param_file.readlines()]

    for input_filename in argv[1:]:
      input_filename = input_filename.strip()
      if input_filename.endswith(".zip"):
        with zipfile.ZipFile(input_filename, "r") as input_dex_zip:
          input_dex_dir = os.path.join(self.tmpdir, str(self.dir_counter))
          os.makedirs(input_dex_dir)
          self.dir_counter += 1

          for input_dex_dex in input_dex_zip.namelist():
            if not input_dex_dex.endswith(".dex"):
              continue

            input_dex_zip.extract(input_dex_dex, input_dex_dir)
            fs_dex = input_dex_dir + "/" + input_dex_dex
            self.ComputeChecksumConcurrently(input_filename, input_dex_dex,
                                             fs_dex)
      elif input_filename.endswith(".dex"):
        self.ComputeChecksumConcurrently(input_filename, None, input_filename)
    # Collect results from all threads
    for t in self.threads_list:
      t.join()

    results = []
    while not self.queue.empty():
      fs_checksum, input_dex_or_zip, zippath = self.queue.get()
      results.append([fs_checksum, input_dex_or_zip, zippath])
    self.AddDexes(results)

    with open(argv[0], "wb") as manifest:
      manifest.write(("\n".join(self.manifest_lines)).encode("utf-8"))


def main(argv):
  with DexmanifestBuilder() as b:
    b.Run(argv[1:])


if __name__ == "__main__":
  main(sys.argv)
