#!/usr/bin/python
# Copyright (c) 2014 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Encompasses a revision file of a package.

A revision file associates an SVN revision with a package. The revision
file will encompass the package information for all package targets. All
packages must have already been built by the buildbot before a revision file
can be made for it.
"""

import hashlib
import json

import archive_info
import error
import package_info
import packages_info


FIELD_PACKAGE_NAME = 'package_name'
FIELD_REVISION = 'revision'
FIELD_PACKAGE_TARGETS = 'package_targets'

FIELD_REVISION_HASH = 'revision_hash'


class RevisionInfo(object):
  """Revision information object describing a set revision for a package."""
  def __init__(self, packages_desc, revision_file=None):
    """Constructor for a RevisionInfo object.

    Args:
      packages_desc: Packages description containing all the necessary packages
                     and package targets to verify a revision file is complete.
      revision_file: Optional JSON file representing a RevisionInfo object.
    """
    assert isinstance(packages_desc, packages_info.PackagesInfo)
    self._packages_desc = packages_desc
    self._revision_num = -1
    self._package_name = None

    # A revision describes all the package_info's for each package target.
    # Every package target in the package_desc must have its revision set
    # for the revision info to be valid.
    self._package_targets = {}

    if revision_file is not None:
      self.LoadRevisionFile(revision_file)

  def __eq__(self, other):
    return (type(self) == type(other) and
            self._revision_num == other._revision_num and
            self._package_name == other._package_name and
            self._package_targets == other._package_targets)

  def _GetRevisionHash(self):
    """Returns a stable hash for a revision file for validation purposes."""
    hash_string = str(self._revision_num)
    hash_string += str(self._package_name)
    for package_target in sorted(self._package_targets):
      package_desc = self._package_targets[package_target]
      archive_list = package_desc.GetArchiveList()

      hash_string += str(package_target)
      for archive in archive_list:
        for field, member in archive.GetArchiveData()._asdict().iteritems():
          hash_string += '[%s:%s]' % (field, member)

    return hashlib.sha1(hash_string).hexdigest()

  def _ValidateRevisionComplete(self):
    """Validate packages to make sure it matches the packages description."""
    if self._package_name is None:
      raise error.Error('Invalid revision information - '
                                  'no package name.')
    elif self._revision_num == -1:
      raise error.Error('Invalid revision information - '
                                  'no revision number')

    package_targets = self._packages_desc.GetPackageTargetsForPackage(
        self._package_name
    )

    if package_targets:
      package_targets = set(package_targets)
      revision_targets = set(self._package_targets.keys())

      if package_targets != revision_targets:
        raise error.Error('Invalid revision information - '
                           'target mismatch:'
                           + '\n%s:' % self._package_name
                           + '\n  Required Target Packages:'
                           + '\n\t' + '\n\t'.join(sorted(package_targets))
                           + '\n  Supplied Target Packages:'
                           + '\n\t' + '\n\t'.join(sorted(revision_targets)))

  def LoadRevisionFile(self, revision_file, skip_hash_verify=False):
    """Loads a revision JSON file into this object.

    Args:
      revision_file: File name for a revision JSON file.
      skip_hash_verify: If True, will skip the hash validation check. This
                        should only be used if a field has been added or
                        removed in order to recalculate the revision hash.
    """
    try:
      with open(revision_file, 'rt') as f:
        revision_json = json.load(f)

      self._package_name = revision_json[FIELD_PACKAGE_NAME]
      self._revision_num = revision_json[FIELD_REVISION]
      self._package_targets = {}

      package_targets = revision_json[FIELD_PACKAGE_TARGETS]
      for package_target, archive_list in package_targets.iteritems():
        self._package_targets[package_target] = package_info.PackageInfo(
            archive_list
        )
    except (TypeError, KeyError) as e:
      raise error.Error('Invalid revision file [%s]: %s' %
                                  (revision_file, e))

    self._ValidateRevisionComplete()

    if not skip_hash_verify:
      hash_value = revision_json[FIELD_REVISION_HASH]
      if self._GetRevisionHash() != hash_value:
        raise error.Error('Invalid revision file [%s] - revision hash check '
            'failed' % revision_file)

  def SaveRevisionFile(self, revision_file):
    """Saves this object to a revision JSON file to be loaded later.

    Args:
      revision_file: File name where revision JSON file will be saved.
    """
    self._ValidateRevisionComplete()

    package_targets = {}
    for package_target, package_desc in self._package_targets.iteritems():
      package_targets[package_target] = package_desc.DumpPackageJson()

    revision_json = {
        FIELD_PACKAGE_NAME: self._package_name,
        FIELD_REVISION: self._revision_num,
        FIELD_PACKAGE_TARGETS: package_targets,
        FIELD_REVISION_HASH: self._GetRevisionHash()
    }

    with open(revision_file, 'wt') as f:
      json.dump(revision_json, f, sort_keys=True,
                indent=2, separators=(',', ': '))

  def SetRevisionNumber(self, revision_num):
    """Sets the current revision number for this object."""
    self._revision_num = revision_num

  def GetRevisionNumber(self):
    """Gets the currently set revision number for this object."""
    return self._revision_num

  def ClearRevisions(self):
    """Clears all package information for this object"""
    self._package_name = None
    self._package_targets = {}

  def SetTargetRevision(self, package_name, package_target, package_desc):
    """Sets a package description for a package target.

    The package description is a package_info object representing the package
    for this particular revision.

    Args:
      package_name: Name of the package this revision object represents.
      package_target: Package target name for the package we are setting.
      package_desc: package_info object representing the package target.
    """
    if self._package_name is None:
      self._package_name = package_name
    elif self._package_name != package_name:
      raise error.Error('Revision information must be all for the '
                        'same package\n'
                        'Original package name: %s\nNew package name: %s'
                        % (self._package_name, package_name))
    self._package_targets[package_target] = package_desc

  def GetPackageInfo(self, package_target):
    """Gets the package description for a particular package target.

    The package description is a package_info object representing the package
    for this particular revision.

    Args:
      package_target: Package target name for which we want the package info.

    Returns:
      A package_info object for the package target, or None for invalid targets.
    """
    return self._package_targets.get(package_target, None)
