# Copyright 2013 Cloudbase Solutions Srl
# 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.
"""
Image caching and management.
"""
import os
import re

from os_win import utilsfactory
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import units
from oslo_utils import uuidutils

import nova.conf
from nova import exception
from nova.i18n import _
from nova import utils
from nova.virt.hyperv import pathutils
from nova.virt import imagecache
from nova.virt import images

LOG = logging.getLogger(__name__)

CONF = nova.conf.CONF


class ImageCache(imagecache.ImageCacheManager):
    def __init__(self):
        super(ImageCache, self).__init__()
        self._pathutils = pathutils.PathUtils()
        self._vhdutils = utilsfactory.get_vhdutils()

    def _get_root_vhd_size_gb(self, instance):
        if instance.old_flavor:
            return instance.old_flavor.root_gb
        else:
            return instance.flavor.root_gb

    def _resize_and_cache_vhd(self, instance, vhd_path):
        vhd_size = self._vhdutils.get_vhd_size(vhd_path)['VirtualSize']

        root_vhd_size_gb = self._get_root_vhd_size_gb(instance)
        root_vhd_size = root_vhd_size_gb * units.Gi

        root_vhd_internal_size = (
                self._vhdutils.get_internal_vhd_size_by_file_size(
                    vhd_path, root_vhd_size))

        if root_vhd_internal_size < vhd_size:
            raise exception.FlavorDiskSmallerThanImage(
                flavor_size=root_vhd_size, image_size=vhd_size)
        if root_vhd_internal_size > vhd_size:
            path_parts = os.path.splitext(vhd_path)
            resized_vhd_path = '%s_%s%s' % (path_parts[0],
                                            root_vhd_size_gb,
                                            path_parts[1])

            lock_path = os.path.dirname(resized_vhd_path)
            lock_name = "%s-cache.lock" % os.path.basename(resized_vhd_path)

            @utils.synchronized(name=lock_name, external=True,
                                lock_path=lock_path)
            def copy_and_resize_vhd():
                if not self._pathutils.exists(resized_vhd_path):
                    try:
                        LOG.debug("Copying VHD %(vhd_path)s to "
                                  "%(resized_vhd_path)s",
                                  {'vhd_path': vhd_path,
                                   'resized_vhd_path': resized_vhd_path})
                        self._pathutils.copyfile(vhd_path, resized_vhd_path)
                        LOG.debug("Resizing VHD %(resized_vhd_path)s to new "
                                  "size %(root_vhd_size)s",
                                  {'resized_vhd_path': resized_vhd_path,
                                   'root_vhd_size': root_vhd_size})
                        self._vhdutils.resize_vhd(resized_vhd_path,
                                                  root_vhd_internal_size,
                                                  is_file_max_size=False)
                    except Exception:
                        with excutils.save_and_reraise_exception():
                            if self._pathutils.exists(resized_vhd_path):
                                self._pathutils.remove(resized_vhd_path)

            copy_and_resize_vhd()
            return resized_vhd_path

    def get_cached_image(self, context, instance, rescue_image_id=None):
        image_id = rescue_image_id or instance.image_ref

        base_vhd_dir = self._pathutils.get_base_vhd_dir()
        base_vhd_path = os.path.join(base_vhd_dir, image_id)

        lock_name = "%s-cache.lock" % image_id

        @utils.synchronized(name=lock_name, external=True,
                            lock_path=base_vhd_dir)
        def fetch_image_if_not_existing():
            vhd_path = None
            for format_ext in ['vhd', 'vhdx']:
                test_path = base_vhd_path + '.' + format_ext
                if self._pathutils.exists(test_path):
                    vhd_path = test_path
                    break

            if not vhd_path:
                try:
                    images.fetch(context, image_id, base_vhd_path)

                    format_ext = self._vhdutils.get_vhd_format(base_vhd_path)
                    vhd_path = base_vhd_path + '.' + format_ext.lower()
                    self._pathutils.rename(base_vhd_path, vhd_path)
                except Exception:
                    with excutils.save_and_reraise_exception():
                        if self._pathutils.exists(base_vhd_path):
                            self._pathutils.remove(base_vhd_path)

            return vhd_path

        vhd_path = fetch_image_if_not_existing()

        # Note: rescue images are not resized.
        is_vhd = vhd_path.split('.')[-1].lower() == 'vhd'
        if CONF.use_cow_images and is_vhd and not rescue_image_id:
            # Resize the base VHD image as it's not possible to resize a
            # differencing VHD. This does not apply to VHDX images.
            resized_vhd_path = self._resize_and_cache_vhd(instance, vhd_path)
            if resized_vhd_path:
                return resized_vhd_path

        if rescue_image_id:
            self._verify_rescue_image(instance, rescue_image_id,
                                      vhd_path)

        return vhd_path

    def _verify_rescue_image(self, instance, rescue_image_id,
                             rescue_image_path):
        rescue_image_info = self._vhdutils.get_vhd_info(rescue_image_path)
        rescue_image_size = rescue_image_info['VirtualSize']
        flavor_disk_size = instance.flavor.root_gb * units.Gi

        if rescue_image_size > flavor_disk_size:
            err_msg = _('Using a rescue image bigger than the instance '
                        'flavor disk size is not allowed. '
                        'Rescue image size: %(rescue_image_size)s. '
                        'Flavor disk size:%(flavor_disk_size)s.') % dict(
                            rescue_image_size=rescue_image_size,
                            flavor_disk_size=flavor_disk_size)
            raise exception.ImageUnacceptable(reason=err_msg,
                                              image_id=rescue_image_id)

    def get_image_details(self, context, instance):
        image_id = instance.image_ref
        return images.get_info(context, image_id)

    def _age_and_verify_cached_images(self, context, all_instances, base_dir):
        for img in self.originals:
            if img in self.used_images:
                # change the timestamp on the image so as to reflect the last
                # time it was used
                self._update_image_timestamp(img)
            elif CONF.image_cache.remove_unused_base_images:
                self._remove_if_old_image(img)

    def _update_image_timestamp(self, image):
        backing_files = self._get_image_backing_files(image)
        for img in backing_files:
            os.utime(img, None)

    def _get_image_backing_files(self, image):
        base_file = self._pathutils.get_image_path(image)
        if not base_file:
            # not vhd or vhdx, ignore.
            return []

        backing_files = [base_file]
        resize_re = re.compile('%s_[0-9]+$' % image)
        for img in self.unexplained_images:
            match = resize_re.match(img)
            if match:
                backing_files.append(self._pathutils.get_image_path(img))

        return backing_files

    def _remove_if_old_image(self, image):
        backing_files = self._get_image_backing_files(image)
        max_age_seconds = (
            CONF.image_cache.remove_unused_original_minimum_age_seconds)

        for img in backing_files:
            age_seconds = self._pathutils.get_age_of_file(img)
            if age_seconds > max_age_seconds:
                LOG.info("Removing old, unused image: %s", img)
                self._remove_old_image(img)

    def _remove_old_image(self, image_path):
        lock_path = os.path.dirname(image_path)
        lock_name = "%s-cache.lock" % os.path.basename(image_path)

        @utils.synchronized(name=lock_name, external=True,
                            lock_path=lock_path)
        def _image_synchronized_remove():
            self._pathutils.remove(image_path)

        _image_synchronized_remove()

    def update(self, context, all_instances):
        base_vhd_dir = self._pathutils.get_base_vhd_dir()

        running = self._list_running_instances(context, all_instances)
        self.used_images = running['used_images'].keys()
        all_files = self._list_base_images(base_vhd_dir)
        self.originals = all_files['originals']
        self.unexplained_images = all_files['unexplained_images']

        self._age_and_verify_cached_images(context, all_instances,
                                           base_vhd_dir)

    def _list_base_images(self, base_dir):
        unexplained_images = []
        originals = []

        for entry in os.listdir(base_dir):
            file_name, extension = os.path.splitext(entry)
            # extension has a leading '.'. E.g.: '.vhdx'
            if extension.lstrip('.').lower() not in ['vhd', 'vhdx']:
                # File is not an image. Ignore it.
                # imagecache will not store images of any other formats.
                continue

            if uuidutils.is_uuid_like(file_name):
                originals.append(file_name)
            else:
                unexplained_images.append(file_name)

        return {'unexplained_images': unexplained_images,
                'originals': originals}
