#
# image.py: Support methods for CD/DVD and ISO image installations.
#
# Copyright (C) 2007  Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import isys, iutil
import os, os.path, stat, string, sys
from constants import *

import gettext
_ = lambda x: gettext.ldgettext("anaconda", x)

import logging
log = logging.getLogger("anaconda")

_arch = iutil.getArch()

def getFreeLoopDev():
    loopdev = iutil.execWithCapture("losetup", ["-f"], fatal=True)
    loopdev = loopdev.strip()
    if not loopdev:
        raise RuntimeError("No free loop device available")

    return loopdev

def findIsoImages(path, messageWindow):
    flush = os.stat(path)
    files = os.listdir(path)
    arch = _arch
    discImages = {}

    for file in files:
        what = path + '/' + file
        if not isys.isIsoImage(what):
            continue

        loopdev = getFreeLoopDev()

        try:
            isys.losetup(loopdev, what, readOnly = 1)
        except SystemError:
            continue

        try:
            isys.mount(loopdev, "/mnt/cdimage", fstype = "iso9660",
                       readOnly = True)
            for num in range(1, 10):
                if os.access("/mnt/cdimage/.discinfo", os.R_OK):
                    f = open("/mnt/cdimage/.discinfo")
                    try:
                        f.readline() # skip timestamp
                        f.readline() # skip release description
                        discArch = string.strip(f.readline()) # read architecture
                        discNum = getDiscNums(f.readline().strip())
                    except:
                        discArch = None
                        discNum = [ 0 ]

                    f.close()

                    if num not in discNum or discArch != arch:
                        continue

                    # if it's disc1, it needs to have images/install.img
                    if (num == 1 and not
                        os.access("/mnt/cdimage/images/install.img", os.R_OK)):
                        log.warning("%s doesn't have a install.img, skipping" %(what,))
                        continue
                    # we only install binary packages, so let's look for a
                    # product/ dir and hope that this avoids getting
                    # discs from the src.rpm set
                    if not os.path.isdir("/mnt/cdimage/%s" %(productPath,)):
                        log.warning("%s doesn't have binary RPMS, skipping" %(what,))
                        continue

                    # warn user if images appears to be wrong size
                    if os.stat(what)[stat.ST_SIZE] % 2048:
                        rc = messageWindow(_("Warning"),
                             _("The ISO image %s has a size which is not "
                               "a multiple of 2048 bytes.  This may mean "
                               "it was corrupted on transfer to this computer."
                               "\n\n"
                               "It is recommended that you exit and abort your "
                               "installation, but you can choose to continue if "
                               "you think this is in error.") % (file,),
                               type="custom", custom_icon="warning",
                               custom_buttons= [_("_Exit installer"),
                                                _("_Continue")])
                        if rc == 0:
                            sys.exit(0)

                    discImages[num] = file

            isys.umount("/mnt/cdimage", removeDir=False)
        except SystemError:
            pass

        isys.unlosetup(loopdev)

    return discImages

def getDiscNums(line):
    # get the disc numbers for this disc
    nums = line.split(",")
    if nums == ['ALL']: # Treat "ALL" DVD as disc 1
        return [1]
    discNums = []
    for num in nums:
        discNums.append(int(num))
    return discNums

def getMediaId(path):
    if os.access("%s/.discinfo" % path, os.R_OK):
        f = open("%s/.discinfo" % path)
        newStamp = f.readline().strip()
        f.close()

        return newStamp
    else:
        return None

# This mounts the directory containing the iso images, and places the
# mount point in /mnt/isodir.
def mountDirectory(methodstr, messageWindow):
    if methodstr.startswith("hd:"):
        method = methodstr[3:]
        options = ''
        if method.count(":") == 1:
            (device, path) = method.split(":")
            fstype = "auto"
        else:
            (device, fstype, path) = method.split(":")

        if not device.startswith("/dev/") and not device.startswith("UUID=") \
           and not device.startswith("LABEL="):
            device = "/dev/%s" % device
    elif methodstr.startswith("nfsiso:"):
        (options, host, path) = iutil.parseNfsUrl(methodstr)
        device = "%s:%s" % (host, path)
        fstype = "nfs"
    else:
        return

    # No need to mount it again.
    if os.path.ismount("/mnt/isodir"):
        return

    while True:
        try:
            isys.mount(device, "/mnt/isodir", fstype=fstype, options=options)
            break
        except SystemError, msg:
            log.error("couldn't mount ISO source directory: %s" % msg)
            ans = messageWindow(_("Couldn't Mount ISO Source"),
                          _("An error occurred mounting the source "
                            "device %s.  This may happen if your ISO "
                            "images are located on an advanced storage "
                            "device like LVM or RAID, or if there was a "
                            "problem mounting a partition.  Click exit "
                            "to abort the installation.")
                          % (device,), type="custom", custom_icon="error",
                          custom_buttons=[_("_Exit"), _("_Retry")])

            if ans == 0:
                sys.exit(0)
            else:
                continue

def mountImage(isodir, tree, discnum, messageWindow, discImages={}):
    if os.path.ismount(tree):
        raise SystemError, "trying to mount already-mounted iso image!"

    if discImages == {}:
        discImages = findIsoImages(isodir, messageWindow)

    loopdev = getFreeLoopDev()

    while True:
        try:
            isoImage = "%s/%s" % (isodir, discImages[discnum])
            isys.losetup(loopdev, isoImage, readOnly = 1)
            isys.mount(loopdev, tree, fstype = 'iso9660', readOnly = True)
            break
        except:
            ans = messageWindow(_("Missing ISO 9660 Image"),
                                _("The installer has tried to mount "
                                  "image #%s, but cannot find it on "
                                  "the hard drive.\n\n"
                                  "Please copy this image to the "
                                  "drive and click Retry.  Click Exit "
                                  "to abort the installation.")
                                  % (discnum,), type="custom",
                                  custom_icon="warning",
                                  custom_buttons=[_("_Exit"), _("_Retry")])
            if ans == 0:
                sys.exit(0)
            elif ans == 1:
                discImages = findIsoImages(isodir, messageWindow)

    return (loopdev, discImages)

# given groupset containing information about selected packages, use
# the disc number info in the headers to come up with message describing
# the required CDs
#
# dialog returns a value of 0 if user selected to abort install
def presentRequiredMediaMessage(anaconda):
    reqcds = anaconda.backend.getRequiredMedia()

    # if only one CD required no need to pop up a message
    if len(reqcds) < 2:
        return

    # check what discs our currently mounted one provides
    if os.access("%s/.discinfo" % anaconda.backend.ayum.tree, os.R_OK):
        discNums = []
        try:
            f = open("%s/.discinfo" % anaconda.backend.ayum.tree)
            stamp = f.readline().strip()
            descr = f.readline().strip()
            arch = f.readline().strip()
            discNums = getDiscNums(f.readline().strip())
            f.close()
        except Exception, e:
            log.critical("Exception reading discinfo: %s" %(e,))

        log.info("discNums is %s" %(discNums,))
        haveall = 0
        s = set(reqcds)
        t = set(discNums)
        if s.issubset(t):
            haveall = 1

        if haveall == 1:
            return

    reqcds.sort()
    reqcds = map(lambda disc: "#%s" % disc, filter(lambda disc: disc != -99, reqcds))
    reqcdstr = ", ".join(reqcds)

    return anaconda.intf.messageWindow(_("Required Install Media"),
               _("The software you have selected to install will require the "
                 "following %(productName)s %(productVersion)s "
                 "discs:\n\n%(reqcdstr)s\nPlease have these ready "
                 "before proceeding with the installation.  If you need to "
                 "abort the installation and exit please select "
                 "\"Reboot\".") % {'productName': product.productName,
                                   'productVersion': product.productVersion,
                                   'reqcdstr': reqcdstr},
                 type="custom", custom_icon="warning",
                 custom_buttons=[_("_Reboot"), _("_Back"), _("_Continue")])

# Find an attached CD/DVD drive with media in it that contains packages,
# and return that device name.
def scanForMedia(tree, storage):
    for dev in storage.devicetree.devices:
        if dev.type != "cdrom":
            continue

        storage.devicetree.updateDeviceFormat(dev)
        try:
            dev.format.mount(mountpoint=tree)
        except:
            continue

        if not verifyMedia(tree, 1):
            dev.format.unmount()
            continue

        return dev.name

    return None

def umountImage(tree, currentMedia, loopdev):
    if currentMedia is not None:
        isys.umount(tree, removeDir=False)
        if loopdev:
            isys.unlosetup(loopdev)

def unmountCD(dev, messageWindow):
    if not dev:
        return

    while True:
        try:
            dev.format.unmount()
            break
        except Exception, e:
            log.error("exception in _unmountCD: %s" %(e,))
            messageWindow(_("Error"),
                          _("An error occurred unmounting the disc.  "
                            "Please make sure you're not accessing "
                            "%s from the shell on tty2 "
                            "and then click OK to retry.")
                          % (dev.path,))

def verifyMedia(tree, discnum, timestamp=None):
    if os.access("%s/.discinfo" % tree, os.R_OK):
        f = open("%s/.discinfo" % tree)

        newStamp = f.readline().strip()

        try:
            descr = f.readline().strip()
        except:
            descr = None

        try:
            arch = f.readline().strip()
        except:
            arch = None

        try:
            discs = getDiscNums(f.readline().strip())
        except:
            discs = [ 0 ]

        f.close()

        if timestamp is not None:
            if newStamp == timestamp and arch == _arch and discnum in discs:
                return True
        else:
            if arch == _arch and discnum in discs:
                return True

    return False
