#
# Copyright 2017 - The Android Open Source Project
#
# 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.

import logging
from vts.runners.host import asserts
from vts.runners.host import const

_PERMISSION_GROUPS = 3  # 3 permission groups: owner, group, all users
_READ_PERMISSION = 4
_WRITE_PERMISSION = 2
_EXECUTE_PERMISSION = 1


def _Test(shell, *args):
    """Executes test command on device.

    Args:
        shell: an instance of the VTS shell.
        *args: strings, the command line arguments.

    Returns:
        boolean, whether the condition is true.
    """
    cmd = "test %s" % " ".join(args)
    results = shell.Execute(cmd)
    return results[const.EXIT_CODE][0] == 0


def Exists(filepath, shell):
    """Determines if a file or directory exists.

    Args:
        filepath: string, the path to a file or a directory.
        shell: an instance of the VTS shell.

    Returns:
        True if exists, False otherwise.
    """
    return _Test(shell, "-e", filepath)


def IsDirectory(path, shell):
    """Determines if a path is a directory.

    Args:
        path: string, a path on device.
        shell: an instance of the VTS shell.

    Returns:
        True if the path is a directory, False otherwise.
    """
    return _Test(shell, "-d", path)


def FindFiles(shell, path, name_pattern, options=None):
    """Searches a path for files on device.

    Args:
        shell: the ShellMirrorObject.
        path: string, the path to search on device.
        name_pattern: string, the file name pattern.
        options: string, other options passed to find command.

    Returns:
        list of strings, the paths to the found files.

    Raises:
        IOError if the pattern contains quotes, or the path does not exist.
    """
    if '"' in name_pattern or "'" in name_pattern:
        raise IOError("File name pattern contains quotes")
    cmd = "find %s -name \"%s\"" % (path, name_pattern)
    if options is not None:
        cmd += " " + options
    results = shell.Execute(cmd)
    logging.debug("%s: Shell command '%s' results: %s", path, cmd, results)

    if results[const.EXIT_CODE][0] != 0:
        raise IOError(results[const.STDERR][0])

    stdout = str(results[const.STDOUT][0])
    # Filter out empty strings before return.
    return filter(None, stdout.strip().split("\n"))


def ReadFileContent(filepath, shell):
    """Read the content of a file and perform assertions.

    Args:
        filepath: string, path to file
        shell: an instance of the VTS shell

    Returns:
        string, content of file

    Raises:
        IOError if the file does not exist.
    """
    cmd = "cat %s" % filepath
    results = shell.Execute(cmd)
    logging.debug("%s: Shell command '%s' results: %s", filepath, cmd, results)

    # checks the exit code
    if results[const.EXIT_CODE][0] != 0:
        raise IOError(results[const.STDERR][0])

    return results[const.STDOUT][0]


def GetPermission(path, shell):
    """Read the file permission bits of a path.

    Args:
        filepath: string, path to a file or directory
        shell: an instance of the VTS shell

    Returns:
        String, octal permission bits for the path

    Raises:
        IOError if the path does not exist or has invalid permission bits.
    """
    cmd = "stat -c %%a %s" % path
    results = shell.Execute(cmd)
    logging.debug("%s: Shell command '%s' results: %s", path, cmd, results)

    # checks the exit code
    if results[const.EXIT_CODE][0] != 0:
        raise IOError(results[const.STDERR][0])

    accessBits = results[const.STDOUT][0].strip()
    if len(accessBits) != 3:
        raise IOError("%s: Wrong number of access bits (%s)" % (path,
                                                                accessBits))
    return accessBits


def _HasPermission(permission_bits, groupIndex, permission):
    """Determines if the permission bits grant a permission to a group.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)
        groupIndex: int, the index of the group into the permissions string.
                    (e.g. 0 is owner group). If set to -1, then all groups are
                    checked.
        permission: the value of the permission.

    Returns:
        True if the group(s) has read permission.

    Raises:
        ValueError if the group or permission bits are invalid
    """
    if groupIndex >= _PERMISSION_GROUPS:
        raise ValueError("Invalid group: %s" % str(groupIndex))

    if len(permission_bits) != _PERMISSION_GROUPS:
        raise ValueError("Invalid permission bits: %s" % str(permission_bits))

    # Define the start/end group index
    start = groupIndex
    end = groupIndex + 1
    if groupIndex < 0:
        start = 0
        end = _PERMISSION_GROUPS

    for i in range(start, end):
        perm = int(permission_bits[i])  # throws ValueError if not an integer
        if perm > 7:
            raise ValueError("Invalid permission bit: %s" % str(perm))
        if perm & permission == 0:
            # Return false if any group lacks the permission
            return False
    # Return true if no group lacks the permission
    return True


def IsReadable(permission_bits):
    """Determines if the permission bits grant read permission to any group.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)

    Returns:
        True if any group has read permission.

    Raises:
        ValueError if the group or permission bits are invalid
    """
    return any([
        _HasPermission(permission_bits, i, _READ_PERMISSION)
        for i in range(_PERMISSION_GROUPS)
    ])


def IsWritable(permission_bits):
    """Determines if the permission bits grant write permission to any group.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)

    Returns:
        True if any group has write permission.

    Raises:
        ValueError if the group or permission bits are invalid
    """
    return any([
        _HasPermission(permission_bits, i, _WRITE_PERMISSION)
        for i in range(_PERMISSION_GROUPS)
    ])


def IsExecutable(permission_bits):
    """Determines if the permission bits grant execute permission to any group.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)

    Returns:
        True if any group has execute permission.

    Raises:
        ValueError if the group or permission bits are invalid
    """
    return any([
        _HasPermission(permission_bits, i, _EXECUTE_PERMISSION)
        for i in range(_PERMISSION_GROUPS)
    ])


def IsReadOnly(permission_bits):
    """Determines if the permission bits grant read-only permission.

    Read-only permission is granted if some group has read access but no group
    has write access.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)

    Returns:
        True if any group has read permission, none have write.

    Raises:
        ValueError if the group or permission bits are invalid
    """
    return IsReadable(permission_bits) and not IsWritable(permission_bits)


def IsWriteOnly(permission_bits):
    """Determines if the permission bits grant write-only permission.

    Write-only permission is granted if some group has write access but no group
    has read access.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)

    Returns:
        True if any group has write permission, none have read.

    Raises:
        ValueError if the group or permission bits are invalid
    """
    return IsWritable(permission_bits) and not IsReadable(permission_bits)


def IsReadWrite(permission_bits):
    """Determines if the permission bits grant read/write permissions.

    Read-write permission is granted if some group has read access and some has
    write access. The groups may be different.

    Args:
        permission_bits: string, the octal permissions string (e.g. 741)

    Returns:
        True if read and write permissions are granted to any group(s).

    Raises:
        ValueError if the group or permission bits are invalid
    """
    return IsReadable(permission_bits) and IsWritable(permission_bits)


def assertPermissionsAndExistence(shell, path, check_permission):
    """Asserts that the specified path exists and has the correct permission.

    Args:
        path: string, path to validate existence and permissions
        check_permission: function which takes unix permissions in octal
                          format and returns True if the permissions are
                          correct, False otherwise.
    """
    asserts.assertTrue(Exists(path, shell), "%s: File does not exist." % path)
    try:
        permission = GetPermission(path, shell)
        asserts.assertTrue(
            check_permission(permission),
            "%s: File has invalid permissions (%s)" % (path, permission))
    except (ValueError, IOError) as e:
        asserts.fail("Failed to assert permissions: %s" % str(e))