# -*- coding: utf-8 -*-


# Copyright (C) 2015  Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA


"""
This module provides classes for manipulating .treeinfo files.
Treeinfo files provide details about installable trees in Fedora composes and media.
"""


__all__ = (
    "TreeInfo",
    "Variant",
    "VARIANT_TYPES",
)


import os
import hashlib
import re

import six

import productmd.common
import productmd.composeinfo


#: supported variant types
VARIANT_TYPES = [
    "variant",
    "optional",
    "addon",
]


def compute_checksum(path, checksum_type):
    checksum = hashlib.new(checksum_type)
    fo = open(path, "rb")
    while True:
        chunk = fo.read(1024**2)
        if not chunk:
            break
        checksum.update(chunk)
    fo.close()
    return checksum.hexdigest().lower()


class TreeInfo(productmd.common.MetadataBase):
    def __init__(self):
        super(productmd.common.MetadataBase, self)
        self.header = Header(self, "productmd.treeinfo")        #: (:class:`productmd.common.Header`) -- Metadata header
        self.release = Release(self)            #: (:class:`.Release`) -- Release details
        self.base_product = BaseProduct(self)   #: (:class:`.BaseProduct`) -- Base product details (optional)
        self.tree = Tree(self)                  #: (:class:`.Tree`) -- Tree details
        self.variants = Variants(self)          #: (:class:`.Variants`) -- Release variants
        self.checksums = Checksums(self)        #: (:class:`.Checksums`) -- Checksums of images included in a tree
        self.images = Images(self)              #: (:class:`.Images`) -- Paths to images included in a tree
        self.stage2 = Stage2(self)              #: (:class:`.Stage2`) -- Stage 2 image path (for Anaconda installer)
        self.media = Media(self)                #: (:class:`.Media`) -- Media set information (optional)

    def __str__(self):
        result = "%s-%s" % (self.release.short, self.release.version)
        if self.release.is_layered:
            result += "-%s-%s" % (self.base_product.short, self.base_product.version)
        variant = sorted(self.variants)[0]
        result += " %s.%s" % (variant, self.tree.arch)
        return result

    def __getitem__(self, name):
        return self.variants[name]

    def __delitem__(self, name):
        del self.variants[name]

    def _get_parser(self):
        return productmd.common.SortedConfigParser()

    def parse_file(self, f):
        # parse file, return parser or dict with data
        f.seek(0)
        parser = productmd.common.SortedConfigParser()
        parser.read_file(f)
        return parser

    def build_file(self, parser, f):
        # build file from parser or dict with data
        parser.write(f)

    def serialize(self, parser):
        self.validate()
        self.header.serialize(parser)
        self.release.serialize(parser)
        if self.release.is_layered:
            self.base_product.serialize(parser)
        self.tree.serialize(parser)
        self.variants.serialize(parser)
        self.checksums.serialize(parser)
        self.images.serialize(parser)
        self.stage2.serialize(parser)
        self.media.serialize(parser)
        # HACK: generate [general] section for compatibility
        general = General(self)
        general.serialize(parser)

    def deserialize(self, parser):
        self.header.deserialize(parser)
        self.release.deserialize(parser)
        if self.release.is_layered:
            self.base_product.deserialize(parser)
        self.tree.deserialize(parser)
        self.variants.deserialize(parser)
        self.checksums.deserialize(parser)
        self.images.deserialize(parser)
        self.stage2.deserialize(parser)
        self.media.deserialize(parser)
        self.validate()
        self.header.set_current_version()
        return parser


class Header(productmd.common.Header):

    def serialize(self, parser):
        self.validate()
        parser.add_section(self._section)
        # write *current* version, because format gets converted on save
        parser.set(self._section, "version", ".".join([str(i) for i in productmd.common.VERSION]))
        parser.set(self._section, "type", self.metadata_type)

    def deserialize(self, parser):
        if parser.has_option(self._section, "version"):
            self.version = parser.get(self._section, "version")
            if self.version_tuple >= (1, 1):
                metadata_type = parser.get(self._section, "type")
                if metadata_type != self.metadata_type:
                    raise ValueError("Invalid metadata type '%s', expected '%s'" % (metadata_type, self.metadata_type))
        self.validate()


class BaseProduct(productmd.common.MetadataBase):
    """
    :class:`.BaseProduct` provides information about operating system a :class:`.Release` runs on.
    """

    def __init__(self, metadata):
        super(BaseProduct, self).__init__()
        self._section = "base_product"
        self._metadata = metadata
        self.name = None                #: (*str*) -- base product name, for example: "Fedora", "Red Hat Enterprise Linux"
        self.short = None               #: (*str*) -- base product short name, for example: "F", "RHEL"
        self.version = None             #: (*str*) -- base product *major* version, for example: "21", "7"

    def _validate_name(self):
        self._assert_type("name", list(six.string_types))

    def _validate_version(self):
        self._assert_type("version", list(six.string_types))
        if re.match('^\d', self.version):
            self._assert_matches_re("version", [r"^\d+(\.\d+)*$"])

    def _validate_short(self):
        self._assert_type("short", list(six.string_types))

    def serialize(self, parser):
        self.validate()
        parser.add_section(self._section)
        parser.set(self._section, "name", self.name)
        parser.set(self._section, "version", self.version)
        parser.set(self._section, "short", self.short)

    def deserialize(self, parser):
        self.name = parser.get(self._section, "name")
        self.version = parser.get(self._section, "version")
        self.short = parser.get(self._section, "short")
        self.validate()


class Release(BaseProduct):

    def __init__(self, metadata):
        super(Release, self).__init__(metadata)
        self._section = "release"
        self.name = None                #: (*str*) -- release name, for example: "Fedora", "Red Hat Enterprise Linux", "Spacewalk"
        self.short = None               #: (*str*) -- release short name, for example: "F", "RHEL", "Spacewalk"
        self.version = None             #: (*str*) -- release version, for example: "21", "7.0", "2.1"
        self.is_layered = False         #: (*bool*) -- typically False for an operating system, True otherwise

    def _validate_is_layered(self):
        self._assert_type("is_layered", [bool])

    def serialize(self, parser):
        self.validate()
        parser.add_section(self._section)
        parser.set(self._section, "name", self.name)
        parser.set(self._section, "version", self.version)
        parser.set(self._section, "short", self.short)
        if self.is_layered:
            parser.set(self._section, "is_layered", "true")

    def deserialize(self, parser):
        if self._metadata.header.version_tuple == (0, 0):
            self.deserialize_0_0(parser)
        elif self._metadata.header.version_tuple <= (0, 3):
            self.deserialize_0_3(parser)
        else:
            self.deserialize_1_0(parser)
        self.validate()

    # pre-productmd treeinfos
    def deserialize_0_0(self, parser):
        self.name = parser.get("general", "family")
        self.version = parser.get("general", "version")
        for i in re.split(r"[-_]", self.version):
            if re.match(r"^\d+(\.\d+)*$", i):
                self.version = i
        if self.name.startswith("Red Hat Enterprise Linux"):
            self.name = "Red Hat Enterprise Linux"
            self.short = "RHEL"
        elif self.name == "Subscription Asset Manager":
            self.short = "SAM"
        elif self.name == "Red Hat Storage":
            self.short = "RHS"
        elif self.name == "JBEAP":
            self.short = "JBEAP"
        elif self.name == "Red Hat Storage Software Appliance":
            self.short = "SSA"
        elif self.name.startswith("Fedora"):
            self.name = "Fedora"
            self.short = "Fedora"
        elif self.name.startswith("CentOS"):
            self.name = "CentOS"
            self.short = "CentOS"
        else:
            self.short = None

    def deserialize_0_3(self, parser):
        self.name = parser.get("product", "name")
        self.version = parser.get("product", "version")
        self.short = parser.get("product", "short")
        if parser.has_option("product", "is_layered"):
            self.is_layered = parser.getboolean("product", "is_layered")

    def deserialize_1_0(self, parser):
        self.name = parser.get(self._section, "name")
        self.version = parser.get(self._section, "version")
        self.short = parser.get(self._section, "short")
        if parser.has_option(self._section, "is_layered"):
            self.is_layered = parser.getboolean(self._section, "is_layered")

    @property
    def major_version(self):
        """
        Version string without the last part.
        For example: version == 1.2.0 -> major_version == 1.2
        """
        if self.version is None:
            return None
        return productmd.common.get_major_version(self.version)

    @property
    def minor_version(self):
        """
        Last part of the version string.
        For example: version == 1.2.0 -> minor_version == 0
        """
        if self.version is None:
            return None
        return productmd.common.get_minor_version(self.version)


# Note: [tree]/variants is read/written in the Variants class
class Tree(productmd.common.MetadataBase):

    def __init__(self, _metadata):
        super(Tree, self).__init__()
        self._section = "tree"
        self._metadata = _metadata
        self.arch = None                #: (*str*) -- tree architecture, for example x86_64
        self.build_timestamp = None     #: (*int*, *float*) -- tree build time timestamp; format: unix time
        self.platforms = set()          #: (*set(str)*), supported platforms; for example x86_64,xen

    def _validate_arch(self):
        self._assert_type("arch", list(six.string_types))
        self._assert_not_blank("arch")

    def _validate_build_timestamp(self):
        self._assert_type("build_timestamp", list(six.integer_types) + [float])
        self._assert_not_blank("build_timestamp")

    def serialize(self, parser):
        self.validate()
        parser.add_section(self._section)
        parser.set(self._section, "arch", self.arch)
        parser.set(self._section, "platforms", ",".join(sorted(self.platforms | set([self.arch]))))
        parser.set(self._section, "build_timestamp", str(self.build_timestamp))

    def deserialize(self, parser):
        if self._metadata.header.version_tuple == (0, 0):
            self.deserialize_0_0(parser)
        else:
            self.deserialize_1_0(parser)
        self.validate()

    def deserialize_0_0(self, parser):
        self.arch = parser.get("general", "arch")
        self.platforms.add(self.arch)

        if self.arch in parser.sections():
            self.platforms.update([i for i in parser.get(self.arch, "platforms").split(",") if i])
        for i in parser.sections():
            if not i.startswith("images-"):
                continue
            i = i[7:]
            if i != self.arch and i.endswith("-%s" % self.arch):
                i = i[:-len(self.arch)-1]
            self.platforms.add(i)

        if parser.has_option("general", "timestamp"):
            self.build_timestamp = int(parser.getfloat("general", "timestamp"))
        else:
            self.build_timestamp = -1

    def deserialize_1_0(self, parser):
        self.arch = parser.get(self._section, "arch")
        self.platforms = set([i for i in parser.get(self._section, "platforms").split(",") if i])
        self.build_timestamp = parser.getint(self._section, "build_timestamp")


class Variants(productmd.composeinfo.VariantBase):
    def __init__(self, metadata):
        super(Variants, self).__init__(metadata)
        self._metadata = metadata

    def __len__(self):
        return len(self.variants)

    def _validate_variants(self):
        self._assert_not_blank("variants")

    def serialize(self, parser):
        self.validate()

        # variant UIDs *should* be identical to IDs at the top level,
        # but sometimes they can differ (Server-optional)
        variant_ids = sorted([i.uid for i in self.variants.values()])

        parser.set("tree", "variants", ",".join(sorted(variant_ids)))

        for variant in self.variants.values():
            variant.serialize(parser)

    def deserialize(self, parser):
        # variant UIDs should be identical to IDs at the top level
        if self._metadata.header.version_tuple == (0, 0):
            variant_ids = self.deserialize_0_0(parser)
        else:
            variant_ids = self.deserialize_1_0(parser)

        for variant_id in variant_ids:
            variant = Variant(self._metadata)
            variant.deserialize(parser, variant_id)
            # handle cases when top-level ID != UID, like $variant-optional
            self.add(variant, variant_id=variant.uid)

        self.validate()

    def deserialize_0_0(self, parser):
        if not parser.has_option("general", "variant") and parser.get("general", "family") == "Red Hat Enterprise Linux Server":
            variant_ids = "Server"
        elif not parser.has_option("general", "variant") and parser.get("general", "family") == "Red Hat Enterprise Linux Client":
            variant_ids = "Client"
        elif not parser.has_option("general", "variant") and parser.get("general", "family") == "CentOS":
            variant_ids = "CentOS"
        else:
            variant_ids = parser.get("general", "variant")
        if variant_ids:
            variant_ids = [variant_ids]
        else:
            variant_ids = [i[8:] for i in parser.sections() if i.startswith("variant-")]
            variant_ids = [i for i in variant_ids if "-" not in i]  # we want only top-level variants
        if not variant_ids:
            variant_ids = [self._metadata.release.short]
        return variant_ids

    def deserialize_1_0(self, parser):
        variant_ids = [i for i in parser.get("tree", "variants").split(",")]
        return variant_ids


class VariantPaths(productmd.common.MetadataBase):
    """
    This class stores paths for a variant in a tree.
    All paths are relative to .treeinfo location.

    **Binary**

        * **packages** -- directory with binary RPMs
        * **repository** -- YUM repository with binary RPMs

    **Source**

        * **source_packages** -- directory with source RPMs
        * **source_repository** -- YUM repository with source RPMs

    **Debug**

        * **debug_packages** -- directory with debug RPMs
        * **debug_repository** -- YUM repository with debug RPMs

    **Others**
        * **identity** -- path to a pem file which identifies a product

    Example::

        variant = ...
        variant.paths.packages = "Packages"
        variant.paths.repository = "."
    """

    def __init__(self, variant):
        self._variant = variant
        self._metadata = self._variant._metadata

        self._fields = [
            # binary
            "packages",
            "repository",

            # source
            "source_packages",
            "source_repository",

            # debug
            "debug_packages",
            "debug_repository",

            # others
            "identity",
        ]

        for name in self._fields:
            setattr(self, name, None)

    def deserialize(self, parser):
        if self._metadata.header.version_tuple == (0, 0):
            self.deserialize_0_0(parser)
        elif self._metadata.header.version_tuple <= (0, 3):
            self.deserialize_0_3(parser)
        else:
            self.deserialize_1_0(parser)

        self.validate()

    # pre-productmd treeinfos
    def deserialize_0_0(self, parser):
        # repository
        lookup = [
            ("variant-%s" % self._variant.id, "repository"),
            ("addon-%s" % self._variant.id, "repository"),
            ("general", "repository"),
        ]
        self.repository = parser.option_lookup(lookup, ".")

        # remove /repodata from repository path
        self.repository = self.repository.rstrip("/") or "."
        if self.repository.endswith("/repodata"):
            self.repository = self.repository[:-9]

        if self.repository == ".":
            if self._metadata.release.short == "RHEL" and self._metadata.release.major_version in ("5", "6"):
                # HACK: repo dirs named by variants on RHEL 5, 6
                self.repository = self._variant.id
            if self._metadata.release.short == "RHEL" and self._metadata.release.major_version in ("3", "4"):
                # HACK: no repos on RHEL 3, 4
                self.repository = None

        # packages
        lookup = [
            ("variant-%s" % self._variant.uid, "packages"),
            ("variant-%s" % self._variant.uid, "packagedir"),
            ("addon-%s" % self._variant.uid, "packages"),
            ("addon-%s" % self._variant.uid, "packagedir"),
            ("variant-%s" % self._variant.id, "packages"),
            ("variant-%s" % self._variant.id, "packagedir"),
            ("addon-%s" % self._variant.id, "packages"),
            ("addon-%s" % self._variant.id, "packagedir"),
            ("general", "packages"),
            ("general", "packagedir"),
            ("general", "packagedirs"),
        ]
        self.packages = parser.option_lookup(lookup, self.repository) or ""
        self.packages = self.packages.rstrip("/") or "."

        if self._metadata.release.short == "RHEL" and self._metadata.release.major_version == "5":
            # HACK: RHEL 5
            self.packages = self._variant.id
        elif self._metadata.release.short == "RHEL" and self._metadata.release.major_version in ("3", "4"):
            # HACK: RHEL 3, 4
            self.packages = "RedHat/RPMS"
        elif self._metadata.release.short == "Fedora":
            # HACK: replace empty packagedir with "Packages" on Fedora
            if self.packages == ".":
                self.packages = "Packages"

        if self._metadata.tree.arch == "src":
            self.source_packages = self.packages
            self.source_repository = self.repository
            self.packages = None
            self.repository = None

        # identity
        lookup = [
            ("variant-%s" % self._variant.uid, "identity"),
            ("addon-%s" % self._variant.uid, "identity"),
            ("variant-%s" % self._variant.id, "identity"),
            ("addon-%s" % self._variant.id, "identity"),
            ("general", "identity"),
        ]
        self.identity = parser.option_lookup(lookup, None)

    def deserialize_0_3(self, parser):
        for field in self._fields:
            lookup = [
                ("variant-%s" % self._variant.uid, field),
                ("variant-%s" % self._variant.id, field),
                ("addon-%s" % self._variant.uid, field),
                ("addon-%s" % self._variant.id, field),
            ]
            value = parser.option_lookup(lookup, None)
            setattr(self, field, value)

        if self._metadata.tree.arch == "src":
            self.source_packages = self.packages
            self.source_repository = self.repository
            self.packages = None
            self.repository = None

    def deserialize_1_0(self, parser):
        for field in self._fields:
            if parser.has_option(self._variant._section, field):
                value = parser.get(self._variant._section, field)
            else:
                value = None
            setattr(self, field, value)

    def serialize(self, parser):
        self.validate()
        for field in self._fields:
            value = getattr(self, field, None)
            if value is not None:
                parser.set(self._variant._section, field, value)


class Variant(productmd.composeinfo.VariantBase):
    def __init__(self, metadata):
        super(Variant, self).__init__(metadata)

        # variant details
        self.id = None          #: (*str*) -- variant ID, for example "Server", "optional"
        self.uid = None         #: (*str*) -- variant UID ($parent_UID.$ID), for example "Server", "Server-optional"
        self.name = None        #: (*str*) -- variant name, for example "Server"
        self.type = None        #: (*str*) -- "variant", "addon", "optional"

        self.parent = None                  #: (:class:`.Variant` or *None*) -- reference to parent :class:`.Variant`
        self.variants = {}                  #: (*dict*) :class:`.Variant`
        self.paths = VariantPaths(self)     #: (:class:`.VariantPaths`) -- relative paths to repositories, packages, etc.

    def __str__(self):
        return self.uid

    def __delitem__(self, name):
        # remove repomd.xml from checksums (but only if exists)
        repository = self[name].paths.repository + "/repodata/repomd.xml"
        super(Variant, self).__delitem__(name)
        if repository in self._metadata.checksums.checksums.keys():
            del self._metadata.checksums.checksums[repository]

    @property
    def arch(self):
        return self._metadata.tree.arch

    @property
    def _section(self):
        if self.type == "addon":
            section = "addon-" + self.uid
        else:
            section = "variant-" + self.uid
        return section

    def _validate_id(self):
        self._assert_type("id", [str])
        if "-" in self.id:
            raise ValueError("Invalid character '-' in variant ID: %s" % self.id)

    def _validate_uid(self):
        if self.parent:
            uid = "%s-%s" % (self.parent.uid, self.id)
        else:
            uid = self.uid
        if self.uid != uid:
            raise ValueError("UID '%s' doesn't align with parent UID '%s'" % (self.uid, uid))

    def _validate_type(self):
        self._assert_value("type", VARIANT_TYPES)

    def deserialize(self, parser, uid, addon=False):
        if not uid:
            raise ValueError("Invalid variant UID value: %s" % uid)

        self.uid = uid
        if addon:
            self.type = "addon"

        # variant details
        if self._metadata.header.version_tuple == (0, 0):
            self.deserialize_0_0(parser, uid, addon=addon)
        elif self._metadata.header.version_tuple <= (0, 3):
            self.deserialize_0_3(parser, uid, addon=addon)
        else:
            self.deserialize_1_0(parser, uid, addon=addon)

        self.paths.deserialize(parser)

    # pre-productmd treeinfo
    def deserialize_0_0(self, parser, uid, addon=False):
        # id, uid
        self.id = uid.split("-")[-1]
        self.uid = uid

        # variant type and section
        sections = [
            "addon-" + self.uid,
            "addon-" + self.id,
            "variant-" + self.uid,
            "variant-" + self.id,
        ]
        for section in sections:
            if parser.has_option(section, "type"):
                self.type = parser.get(section, "type")
                break
            if parser.has_section(section):
                if "addon" in section:
                    self.type = "addon"
                elif "optional" in self.id:
                    self.type = "optional"
                else:
                    self.type = "variant"
                break

        if not self.type:
            # no variant/addon section, fallback to general
            if addon:
                self.type = "addon"
            else:
                self.type = "variant"
            self.name = self._metadata.release.name
            self.uid = uid
            self.id = uid.rsplit("-")[-1]

        # name
        if parser.has_option(section, "name"):
            self.name = parser.get(section, "name")
        else:
            self.name = self.id

        if self.type == "variant":
            lookup = [
                (section, "addons"),
                (section, "variants"),
                ("general", "addons"),
            ]
            addons = parser.option_lookup(lookup, "").split(",")
            addons = [i for i in addons if i]
            if self._metadata.release.short == "RHEL" and self._metadata.release.major_version == "5":
                # workaround for RHEL 5 - add addons
                if self.uid == "Client":
                    addons = ["VT", "Workstation"]
                if self.uid == "Server":
                    if self.arch in ("i386", "ia64", "x86_64"):
                        addons = ["Cluster", "ClusterStorage", "VT"]
                    elif self.arch == "ppc":
                        if self._metadata.release.minor_version == "0":
                            # no addons on RHEL 5.0 Server.ppc
                            addons = []
                        else:
                            addons = ["Cluster", "ClusterStorage"]
                    elif self.arch == "s390x":
                        addons = []
            for addon_id in addons:
                addon_uid = addon_id
                if not addon_uid.startswith("%s-" % self.uid):
                    addon_uid = "%s-%s" % (self.uid, addon_uid)
                addon = Variant(self._metadata)
                addon.deserialize(parser, addon_uid)
                # HACK: for RHEL 5 addons
                addon.type = "addon"
                self.add(addon)

    def deserialize_0_3(self, parser, uid, addon=False):
        section = "variant-%s" % uid
        if not parser.has_section(section):
            section = "addon-%s" % uid
        self.id = parser.get(section, "id")
        self.uid = parser.get(section, "uid")
        self.name = parser.get(section, "name")
        self.type = parser.get(section, "type")

        # child addons
        addons = ""
        if parser.has_option(section, "addons"):
            addons = parser.get(section, "addons")
        elif parser.has_option(section, "variants"):
            addons = parser.get(section, "variants")
        if addons:
            variant_uids = [i for i in addons.split(",") if i]
            for variant_uid in variant_uids:
                variant = Variant(self._metadata)
                variant.deserialize(parser, variant_uid, addon=True)
                self.add(variant)

    def deserialize_1_0(self, parser, uid, addon=False):
        self.id = parser.get(self._section, "id")
        self.uid = parser.get(self._section, "uid")
        self.name = parser.get(self._section, "name")
        self.type = parser.get(self._section, "type")

        # child addons
        if parser.has_option(self._section, "addons"):
            variant_uids = [i for i in parser.get(self._section, "addons").split(",") if i]
            for variant_uid in variant_uids:
                variant = Variant(self._metadata)
                variant.deserialize(parser, variant_uid, addon=True)
                self.add(variant)

    def serialize(self, parser):
        self.validate()
#        print "SERIALIZE", self._section, self.type
        parser.add_section(self._section)

        # variant details
        parser.set(self._section, "id", self.id)
        parser.set(self._section, "uid", self.uid)
        parser.set(self._section, "name", self.name)
        parser.set(self._section, "type", self.type)

        # paths
        self.paths.serialize(parser)

        # parent
        if self.parent:
            parser.set(self._section, "parent", self.parent.uid)

        # child variants
        variant_uids = set()
        for variant in self.variants.values():
            variant.serialize(parser)
            variant_uids.add(variant.uid)
        if variant_uids:
            parser.set(self._section, "addons", ",".join(sorted(variant_uids)))


class Images(productmd.common.MetadataBase):

    def __init__(self, metadata):
        super(Images, self).__init__()
        self._metadata = metadata
        self.images = {}

    def __getitem__(self, platform):
        return self.images[platform]

    def _fix_path(self, path):
        if self._metadata.header.version_tuple == (0, 0):
            if path.startswith("/"):
                if "/os/" in path:
                    path = path[path.find("/os/")+4:]
                else:
                    path = path.lstrip("/")
        return path

    @property
    def platforms(self):
        """Return all platforms with available images"""
        return sorted(self.images.keys())

    def serialize(self, parser):
        if not self.images:
            return
        self.validate()
        for platform in self.images:
            section = "images-%s" % platform
            parser.add_section(section)
            for image, path in self.images[platform].items():
                parser.set(section, image, path)

    def deserialize(self, parser):
        for section in parser.sections():
            if not section.startswith("images-"):
                continue
            platform = section[7:]
            if platform != self._metadata.tree.arch and platform.endswith("-%s" % self._metadata.tree.arch):
                platform = platform[:-len(self._metadata.tree.arch)-1]
            self.images[platform] = {}
            for image, path in parser.items(section):
                path = parser.get(section, image)  # re-read path to populate 'seen' records in tests
                self.images[platform][image] = self._fix_path(path)
        self.validate()

    def _validate_image_paths(self):
        for platform in self.images:
            for image, path in self.images[platform].items():
                if path.startswith("/"):
                    raise ValueError("Only relative paths are allowed for images: %s" % path)

    def _validate_platforms(self):
        for platform in self.platforms:
            if platform not in self._metadata.tree.platforms:
                raise ValueError("Platform has images but is not referenced in platform list: %s, %s"
                                 % (platform, self._metadata.tree.platforms))


class Stage2(productmd.common.MetadataBase):
    def __init__(self, metadata):
        super(Stage2, self).__init__()
        self._section = "stage2"
        self._metadata = metadata
        self.mainimage = None           #: (*str*) -- relative path to Anaconda stage2 image
        self.instimage = None           #: (*str*) -- relative path to Anaconda instimage (obsolete)

    def __getitem__(self, name):
        getattr(self, name)

    def _fix_path(self, path):
        if self._metadata.header.version_tuple == (0, 0):
            if path.startswith("/"):
                if "/os/" in path:
                    path = path[path.find("/os/")+4:]
                else:
                    path = path.lstrip("/")
        return path

    def serialize(self, parser):
        if not self.mainimage and not self.instimage:
            return
        self.validate()
        parser.add_section(self._section)
        if self.mainimage:
            parser.set(self._section, "mainimage", self.mainimage)
        if self.instimage:
            parser.set(self._section, "instimage", self.instimage)

    def deserialize(self, parser):
        if parser.has_option(self._section, "mainimage"):
            self.mainimage = self._fix_path(parser.get(self._section, "mainimage"))
        if parser.has_option(self._section, "instimage"):
            self.instimage = self._fix_path(parser.get(self._section, "instimage"))
        self.validate()

    def _validate_mainimage(self):
        if self.mainimage:
            self._assert_type("mainimage", list(six.string_types))
            if self.mainimage.startswith("/"):
                raise ValueError("Only relative paths are allowed for images: %s" % self.mainimage)

    def _validate_platforms(self):
        pass


class Checksums(productmd.common.MetadataBase):

    def __init__(self, metadata):
        super(Checksums, self).__init__()
        self._section = "checksums"
        self._metadata = metadata
        self.checksums = {}

    def __getitem__(self, name):
        return self.checksums[name]

    def _fix_path(self, path):
        if self._metadata.header.version_tuple == (0, 0):
            if path.startswith("/"):
                if "/os/" in path:
                    path = path[path.find("/os/")+4:]
                else:
                    path = path.lstrip("/")
        return path

    def serialize(self, parser):
        self.validate()
        if not self.checksums:
            return
        parser.add_section(self._section)
        for path, (checksum_type, checksum) in self.checksums.items():
            parser.set(self._section, path, "%s:%s" % (checksum_type, checksum))

    def deserialize(self, parser):
        if parser.has_section(self._section):
            for path, value in parser.items(self._section):
                path = self._fix_path(path)
                if ":" not in value:
                    if len(value) == 32:
                        checksum_type, checksum = "md5", value
                    elif len(value) == 40:
                        checksum_type, checksum = "sha1", value
                    elif len(value) == 64:
                        checksum_type, checksum = "sha256", value
                else:
                    checksum_type, checksum = value.split(":")
                self.checksums[path] = (checksum_type, checksum)
        self.validate()

    def _check_checksum_paths(self):
        for path in self.checksums:
            if path.startswith("/"):
                raise ValueError("Only relative paths are allowed for checksums: %s" % path)

    def add(self, relative_path, checksum_type, checksum_value=None, root_dir=None):
        if relative_path.startswith("/"):
            raise ValueError("Relative path expected: %s" % relative_path)
        relative_path = os.path.normpath(relative_path)
        if not checksum_value:
            absolute_path = os.path.join(root_dir, relative_path)
            checksum_value = compute_checksum(absolute_path, checksum_type)
        self.checksums[relative_path] = [checksum_type, checksum_value]


class Media(productmd.common.MetadataBase):

    def __init__(self, metadata):
        super(Media, self).__init__()
        self._section = "media"
        self._metadata = metadata
        self.discnum = None             #: disc number
        self.totaldiscs = None          #: number of discs in media set

    def _validate_discnum(self):
        self._assert_type("discnum", list(six.integer_types) + [type(None)])

    def _validate_totaldiscs(self):
        self._assert_type("totaldiscs", list(six.integer_types) + [type(None)])

    def serialize(self, parser):
        if not self.discnum and not self.totaldiscs:
            return
        self.validate()
        parser.add_section(self._section)
        parser.set(self._section, "discnum", str(int(self.discnum)))
        parser.set(self._section, "totaldiscs", str(int(self.totaldiscs)))

    def deserialize(self, parser):
        if self._metadata.header.version_tuple == (0, 0):
            self.deserialize_0_0(parser)
        else:
            self.deserialize_1_0(parser)
        self.validate()

    def deserialize_0_0(self, parser):
        if parser.has_option("general", "discnum") or parser.has_option("general", "totaldiscs"):
            if parser.has_option("general", "discnum"):
                self.discnum = parser.getint("general", "discnum")
            else:
                self.discnum = 1
            if parser.has_option("general", "totaldiscs"):
                self.totaldiscs = parser.getint("general", "totaldiscs")
            else:
                self.totaldiscs = self.discnum

    def deserialize_1_0(self, parser):
        if parser.has_section(self._section):
            self.discnum = parser.getint(self._section, "discnum")
            self.totaldiscs = parser.getint(self._section, "totaldiscs")


class General(productmd.common.MetadataBase):

    def __init__(self, metadata):
        super(General, self).__init__()
        self._section = "general"
        self._metadata = metadata

    def serialize(self, parser):
        parser.add_section(self._section)
        parser.set(self._section, "; WARNING.0", "This section provides compatibility with pre-productmd treeinfos.")
        parser.set(self._section, "; WARNING.1", "Read productmd documentation for details about new format.")
        parser.set(self._section, "name", "%s %s" % (self._metadata.release.name, self._metadata.release.version))
        parser.set(self._section, "family", self._metadata.release.name)
        parser.set(self._section, "version", self._metadata.release.version)

        parser.set(self._section, "arch", self._metadata.tree.arch)
        parser.set(self._section, "platforms", ",".join(sorted(self._metadata.tree.platforms | set([self._metadata.tree.arch]))))
        parser.set(self._section, "timestamp", str(int(self._metadata.tree.build_timestamp)))

        variants = list(self._metadata.variants)
        variants.sort()
        parser.set(self._section, "variants", ",".join(variants))

        # HACK: if there are more variants, use the first variant
        variant = variants[0]
        parser.set(self._section, "variant", variant)

        # packages
        if self._metadata.variants[variant].paths.packages is not None:
            parser.set(self._section, "packagedir", self._metadata.variants[variant].paths.packages)
        elif self._metadata.tree.arch == "src" and self._metadata.variants[variant].paths.source_packages is not None:
            parser.set(self._section, "packagedir", self._metadata.variants[variant].paths.source_packages)

        # repository
        if self._metadata.variants[variant].paths.repository is not None:
            parser.set(self._section, "repository", self._metadata.variants[variant].paths.repository)
        elif self._metadata.tree.arch == "src" and self._metadata.variants[variant].paths.source_repository is not None:
            parser.set(self._section, "repository", self._metadata.variants[variant].paths.source_repository)
