
from __future__ import absolute_import
import json
import logging
import os
import re

from vdsm.common import cmdutils
from vdsm.common import commands
from vdsm.common import exception
from vdsm.config import config
from vdsm.storage import operation

_qemuimg = cmdutils.CommandPath("qemu-img", "/usr/bin/qemu-img")

_log = logging.getLogger("QemuImg")


class FORMAT:
    QCOW2 = "qcow2"
    QCOW = "qcow"
    QED = "qed"
    RAW = "raw"
    VMDK = "vmdk"

_QCOW2_COMPAT_SUPPORTED = ("0.10", "1.1")


class PREALLOCATION:

    OFF = "off"

    METADATA = "metadata"

    FALLOC = "falloc"

    FULL = "full"


def supports_compat(compat):
    return compat in _QCOW2_COMPAT_SUPPORTED


class InvalidOutput(cmdutils.Error):
    msg = ("Commmand {self.cmd} returned invalid output: {self.out}: "
           "{self.reason}")

    def __init__(self, cmd, out, reason):
        self.cmd = cmd
        self.out = out
        self.reason = reason


def info(image, format=None, unsafe=False, trusted_image=True):
    cmd = [_qemuimg.cmd, "info", "--output", "json"]

    if format:
        cmd.extend(("-f", format))

    if unsafe:
        cmd.append('-U')

    cmd.append(image)

    if not trusted_image:
        cmd = cmdutils.prlimit(cmd, cpu_time=30, address_space=1024**3)

    out = _run_cmd(cmd)

    try:
        qemu_info = _parse_qemuimg_json(out)
    except ValueError:
        raise InvalidOutput(cmd, out, "Failed to process qemu-img output")

    try:
        info = {
            'format': qemu_info['format'],
            'virtualsize': qemu_info['virtual-size'],
        }
    except KeyError as key:
        raise InvalidOutput(cmd, out, "Missing field: %r" % key)

    if 'actual-size' in qemu_info:
        info['actualsize'] = qemu_info['actual-size']
    if 'cluster-size' in qemu_info:
        info['clustersize'] = qemu_info['cluster-size']
    if 'backing-filename' in qemu_info:
        info['backingfile'] = qemu_info['backing-filename']
    if qemu_info['format'] == FORMAT.QCOW2:
        try:
            info['compat'] = qemu_info['format-specific']['data']['compat']
        except KeyError:
            raise InvalidOutput(cmd, out, "'compat' expected but not found")

    return info


def measure(image, format=None, output_format=None):
    cmd = [_qemuimg.cmd, "measure", "--output", "json"]

    if format:
        cmd.extend(("-f", format))

    if output_format:
        cmd.extend(("-O", output_format))

    cmd.append(image)
    out = _run_cmd(cmd)
    try:
        qemu_measure = _parse_qemuimg_json(out)
    except ValueError:
        raise InvalidOutput(cmd, out, "Failed to process qemu-img output")

    for key in ("required", "fully-allocated"):
        if key not in qemu_measure:
            raise InvalidOutput(cmd, out, "Missing field: %r" % key)

    return qemu_measure


def create(image, size=None, format=None, qcow2Compat=None,
           backing=None, backingFormat=None, preallocation=None, unsafe=False):
    cmd = [_qemuimg.cmd, "create"]
    cwdPath = None

    if format:
        cmd.extend(("-f", format))
        if format == FORMAT.QCOW2:
            qcow2Compat = _validate_qcow2_compat(qcow2Compat)
            cmd.extend(('-o', 'compat=' + qcow2Compat))

    if backing:
        if not os.path.isabs(backing):
            cwdPath = os.path.dirname(image)
        cmd.extend(("-b", backing))

    if backingFormat:
        cmd.extend(("-F", backingFormat))

    if preallocation:
        cmd.extend(("-o", "preallocation=" +
                    _get_preallocation(preallocation, format)))

    if unsafe:
        cmd.append('-u')

    cmd.append(image)

    if size is not None:
        cmd.append(str(size))

    return operation.Command(cmd, cwd=cwdPath)


def check(image, format=None):
    cmd = [_qemuimg.cmd, "check", "--output", "json"]

    if format:
        cmd.extend(("-f", format))

    cmd.append(image)
    try:
        out = _run_cmd(cmd)
    except cmdutils.Error as e:
        if e.rc != 3:
            raise
        out = e.out
    try:
        qemu_check = _parse_qemuimg_json(out)
    except ValueError:
        raise InvalidOutput(cmd, out, "Failed to process qemu-img output")
    if "leaks" in qemu_check:
        _log.warning("%d leaked clusters found on the image",
                     qemu_check["leaks"])
    try:
        return {"offset": qemu_check["image-end-offset"]}
    except KeyError:
        raise InvalidOutput(cmd, out, "unable to parse qemu-img check output")


def convert(srcImage, dstImage, srcFormat=None, dstFormat=None,
            dstQcow2Compat=None, backing=None, backingFormat=None,
            preallocation=None, compressed=False, unordered_writes=False,
            create=True):
    cmd = [_qemuimg.cmd, "convert", "-p", "-t", "none", "-T", "none"]
    options = []
    cwdPath = None

    if not create:
        cmd.append("-n")

    if srcFormat:
        cmd.extend(("-f", srcFormat))

    cmd.append(srcImage)

    if dstFormat:
        cmd.extend(("-O", dstFormat))
        if dstFormat == FORMAT.QCOW2:
            qcow2Compat = _validate_qcow2_compat(dstQcow2Compat)
            options.append('compat=' + qcow2Compat)
        if preallocation:
            value = _get_preallocation(preallocation, dstFormat)
            options.append("preallocation=" + value)

    if backing:
        if not os.path.isabs(backing):
            cwdPath = os.path.dirname(srcImage)

        options.append('backing_file=' + str(backing))

        if backingFormat:
            options.append('backing_fmt=' + str(backingFormat))

    if options:
        cmd.extend(('-o', ','.join(options)))

    if compressed:
        cmd.append('-c')

    if unordered_writes:
        cmd.append('-W')

    cmd.append(dstImage)

    return ProgressCommand(cmd, cwd=cwdPath)


def commit(top, topFormat, base=None):
    cmd = [_qemuimg.cmd, "commit", "-p", "-t", "none"]

    if base:
        cmd.extend(("-b", base))
    else:
        cmd.append("-d")

    cmd.extend(("-f", topFormat))

    cmd.append(top)

    workdir = os.path.dirname(top)
    return ProgressCommand(cmd, cwd=workdir)


def map(image):
    cmd = [_qemuimg.cmd, "map", "--output", "json", image]
    workdir = os.path.dirname(image)
    out = _run_cmd(cmd, cwd=workdir)
    try:
        return json.loads(out.decode("utf8"))
    except ValueError:
        raise InvalidOutput(cmd, out, "Failed to process qemuimg map output")


def amend(image, compat):
    if compat not in _QCOW2_COMPAT_SUPPORTED:
        raise ValueError("Invalid compat version %r" % compat)

    workdir = os.path.dirname(image)
    cmd = [_qemuimg.cmd, "amend", "-o", "compat=" + compat, image]
    _run_cmd(cmd, cwd=workdir)


class ProgressCommand(object):

    REGEXPR = re.compile(br'\s*\(([\d.]+)/100%\)\s*')

    def __init__(self, cmd, cwd=None):
        self._operation = operation.Command(cmd, cwd=cwd)
        self._progress = 0.0

    def run(self):
        out = bytearray()
        for data in self._operation.watch():
            out += data
            self._update_progress(out)

    def abort(self):
        self._operation.abort()

    @property
    def progress(self):
        return self._progress

    def _update_progress(self, out):
        try:
            idx = out.rindex(b'\r')
        except ValueError:
            return

        valid_progress = out[:idx]
        last_progress = valid_progress.rsplit(b'\r', 1)[-1]

        del out[:idx + 1]

        m = self.REGEXPR.match(last_progress)
        if m is None:
            raise ValueError('Unable to parse: "%r"' % last_progress)

        self._progress = float(m.group(1))


def resize(image, newSize, format=None):
    cmd = [_qemuimg.cmd, "resize"]

    if format:
        cmd.extend(("-f", format))

    cmd.extend((image, str(newSize)))
    _run_cmd(cmd)


def rebase(image, backing, format=None, backingFormat=None, unsafe=False):
    cmd = [_qemuimg.cmd, "rebase", "-t", "none", "-T", "none"]

    if unsafe:
        cmd.extend(("-u",))

    if format:
        cmd.extend(("-f", format))

    if backingFormat:
        cmd.extend(("-F", backingFormat))

    cmd.extend(("-b", backing, image))

    cwdPath = None if os.path.isabs(backing) else os.path.dirname(image)

    return operation.Command(cmd, cwd=cwdPath)


def default_qcow2_compat():
    value = config.get('irs', 'qcow2_compat')
    if value not in _QCOW2_COMPAT_SUPPORTED:
        raise exception.InvalidConfiguration(
            reason="Unsupported value for irs:qcow2_compat",
            qcow2_compat=value,
            supported_values=_QCOW2_COMPAT_SUPPORTED)
    return value


def _parse_qemuimg_json(output):
    obj = json.loads(output.decode("utf8"))
    if not isinstance(obj, dict):
        raise ValueError("not a JSON object")
    return obj


def _validate_qcow2_compat(value):
    if value is None:
        return default_qcow2_compat()
    if value not in _QCOW2_COMPAT_SUPPORTED:
        raise ValueError("Invalid compat version %r" % value)
    return value


def _get_preallocation(value, format):
    if value not in (PREALLOCATION.OFF,
                     PREALLOCATION.FALLOC,
                     PREALLOCATION.FULL,
                     PREALLOCATION.METADATA):
        raise ValueError("Invalid preallocation type %r" % value)
    if (value == PREALLOCATION.METADATA and
            format not in (FORMAT.QCOW2, FORMAT.QCOW)):
        raise ValueError("Unsupported preallocation mode %r for format %r" %
                         (value, format))
    return value


def _run_cmd(cmd, cwd=None):
    rc, out, err = commands.execCmd(cmd, raw=True, cwd=cwd)
    if rc != 0:
        raise cmdutils.Error(cmd, rc, out, err)
    return out
