#
# geometry.py
# Python bindings for libparted (built on top of the _ped Python module).
#
# Copyright The pyparted Project Authors
# SPDX-License-Identifier: GPL-2.0-or-later
#

import math
import warnings

import parted
import _ped

from parted.decorators import localeC


class Geometry(object):
    """Geometry()

    Geometry represents a region on a device in the system - a disk or
    partition.  It is expressed in terms of a starting sector and a length.
    Many methods (read and write methods in particular) throughout pyparted
    take in a Geometry object as an argument."""

    @localeC
    def __init__(
        self, device=None, start=None, length=None, end=None, PedGeometry=None
    ):
        """Create a new Geometry object for the given _ped.Device that extends
        for length sectors from the start sector.  Optionally, an end sector
        can also be provided."""
        if PedGeometry:
            self.__geometry = PedGeometry

            if device is None:
                self._device = parted.Device(PedDevice=self.__geometry.dev)
            else:
                self._device = device
        elif not end:
            self._device = device
            self.__geometry = _ped.Geometry(self.device.getPedDevice(), start, length)
        elif not length and (end > start):
            self._device = device
            self.__geometry = _ped.Geometry(
                self.device.getPedDevice(), start, (end - start + 1), end=end
            )
        elif start and length and end and (end > start):
            self._device = device
            self.__geometry = _ped.Geometry(
                self.device.getPedDevice(), start, length, end=end
            )
        else:
            raise parted.GeometryException(
                "must specify PedGeometry or (device, start, length) or (device, start, end) or (device, start, length, end)"
            )

    def __eq__(self, other):
        return not self.__ne__(other)

    def __ne__(self, other):
        if not isinstance(self, other.__class__):
            return True

        return (
            self.device != other.device
            or self.start != other.start
            or self.length != other.length
        )

    def __str__(self):
        s = (
            "parted.Geometry instance --\n"
            "  start: %(start)s  end: %(end)s  length: %(length)s\n"
            "  device: %(device)r  PedGeometry: %(ped)r"
            % {
                "start": self.start,
                "end": self.end,
                "length": self.length,
                "device": self.device,
                "ped": self.__geometry,
            }
        )
        return s

    @property
    def device(self):
        """The Device this geometry describes."""
        return self._device

    start = property(
        lambda s: s.__geometry.start, lambda s, v: s.__geometry.set_start(v)
    )
    end = property(lambda s: s.__geometry.end, lambda s, v: s.__geometry.set_end(v))
    length = property(
        lambda s: s.__geometry.length,
        lambda s, v: s.__geometry.set(s.__geometry.start, v),
    )

    @localeC
    def check(self, offset, granularity, count, timer=None):
        """Check the region described by self for errors on the disk.
        offset -- The beginning of the region to check, in sectors from the
                  start of the geometry.
        granularity -- How sectors should be grouped together
        count -- How many sectors from the region to check."""
        if not timer:
            return self.__geometry.check(offset, granularity, count)
        else:
            return self.__geometry.check(offset, granularity, count, timer)

    @localeC
    def contains(self, b):
        """Return whether Geometry b is contained entirely within self and on
        the same physical device."""
        return self.__geometry.test_inside(b.getPedGeometry())

    @localeC
    def containsSector(self, sector):
        """Return whether the sectory is contained entirely within self."""
        return self.__geometry.test_sector_inside(sector)

    @localeC
    def getSize(self, unit="MB"):
        """Return the size of the geometry in the unit specified.  The unit
        is given as a string corresponding to one of the following
        abbreviations:  b (bytes), KB (kilobytes), MB (megabytes), GB
        (gigabytes), TB (terabytes).  An invalid unit string will raise a
        SyntaxError exception.  The default unit is MB."""
        warnings.warn("use the getLength method", DeprecationWarning)
        lunit = unit.lower()
        size = self.length * self.device.sectorSize

        if lunit not in parted._exponent.keys():
            raise SyntaxError("invalid unit %s given" % (unit))

        return size / math.pow(1024.0, parted._exponent[lunit])

    @localeC
    def getLength(self, unit="sectors"):
        """Return the length of the geometry in sectors. Optionally, a SI or
        IEC prefix followed by a 'B' may be given in order to convert the
        length into bytes. The allowed values include B, kB, MB, GB, TB, KiB,
        MiB, GiB, and TiB."""
        sectors = self.length
        if unit == "sectors":
            return sectors
        return parted.formatBytes(sectors * self.device.sectorSize, unit)

    @localeC
    def intersect(self, b):
        """Return a new Geometry describing the region common to both self
        and Geometry b.  Raises ArithmeticError if the regions do not
        intersect."""
        return Geometry(PedGeometry=self.__geometry.intersect(b.getPedGeometry()))

    @localeC
    def map(self, src, sector):
        """Given a Geometry src that overlaps with self and a sector inside src,
        this method translates the address of the sector into an address
        inside self.  If self does not contain sector, ArithmeticError will
        be raised."""
        return parted.Geometry(
            PedGeometry=self.__geometry.map(src.getPedGeometry(), sector)
        )

    @localeC
    def overlapsWith(self, b):
        """Return whether self and b are on the same device and share at least
        some of the same region."""
        try:
            self.__geometry.intersect(b.getPedGeometry())
            return True
        except ArithmeticError:
            return False

    @localeC
    def read(self, offset, count):
        """Read data from the region described by self.
        offset -- The number of sectors from the beginning of the region
                  (not the beginning of the disk) to read.
        count  -- The number of sectors to read."""
        return self.__geometry.read(offset, count)

    @localeC
    def sync(self, fast=False):
        """Flushes all caches on the device described by self.  If fast is
        True, the flush will be quicked by cache coherency is not
        guaranteed."""
        if fast:
            return self.__geometry.sync_fast()
        else:
            return self.__geometry.sync()

    @localeC
    def write(self, buf, offset, count):
        """Write data into the region described by self.
        buf    -- The data to be written.
        offset -- Where to start writing to region, expressed as the number
                  of sectors from the start of the region (not the disk).
        count  -- How many sectors of buf to write out."""
        return self.__geometry.write(buf, offset, count)

    def getPedGeometry(self):
        """Return the _ped.Geometry object contained in this Geometry.
        For internal module use only."""
        return self.__geometry
