#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Package that contains string manipulation functions
"""

## \package burger.strutils

from __future__ import absolute_import, print_function, unicode_literals

import sys
import os
import string
import re
import csv
import fnmatch
import platform

## True if the interpreter is Python 2.x
PY2 = sys.version_info[0] == 2

## True if the interpreter is Python 3.x or higher
PY3_OR_HIGHER = sys.version_info[0] >= 3

## True if the interpreter is Python 3.3 or higher
PY3_3_OR_HIGHER = sys.version_info >= (3, 3, 0)

## True if the interpreter is Python 3.4 or higher
PY3_4_OR_HIGHER = sys.version_info >= (3, 4, 0)

## True if the interpreter is Python 3.5 or higher
PY3_5_OR_HIGHER = sys.version_info >= (3, 5, 0)

## True if the interpreter is PyPy
PYPY = platform.python_implementation() == 'PyPy'

## Valid characters for windows filenames without quoting
_WINDOWSSAFESET = frozenset(string.ascii_letters + string.digits + '_-.:\\')

## Valid characters for macOS and Linux files without quoting
_LINUXSAFESET = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')

## Regex to match comma and quotes
_RE_COMMA_QUOTES = re.compile(r"\\.|[\"',]", re.DOTALL)

########################################


def unicode_print(input_string):
    """
    Handle printing a unicode string to stdout

    On some platforms, printing a unicode string will trigger
    a UnicodeEncodeError exception. In these cases, handle the
    exception and recode the string to the native string
    encoding.

    Args:
        input_string: A unicode string to print to stdout.
    """

    # Print the string, if no exception, exit
    try:
        print(input_string)
    except UnicodeEncodeError:
        # Ensure it's encoded to utf-8
        encoded = input_string.encode('utf-8')
        if PY2:

            # Python 2.x only accepts this as input
            print(encoded)
        else:

            # Python 3.x and higher will allow remapping to
            # selected character encoding
            print(encoded.decode(sys.stdout.encoding))

########################################


try:
    # Test if basestring exists (Only in Python 2.x)
    basestring                                        # pylint: disable=W0104,basestring-builtin
    ## Internal type to pass to isinstance() for is_string()
    _IS_STRING_TEST = basestring                    # pylint: disable=C0103,basestring-builtin

except NameError:
    # Python 3 or later
    _IS_STRING_TEST = (str, bytes)


def is_string(item):
    """
    Return True if input is a string object

    Test the input if it's either an instance of
    basestring in Python 2.x or (str, bytes) in Python 3.x

    Args:
        item: Object to test
    Returns:
        True if the object is a string instance, False if not.
    """

    return isinstance(item, _IS_STRING_TEST)

########################################


def convert_to_array(input_array):
    """
    Convert a string to a string array (list)

    If the input is None, return an empty list. If
    it's a string, convert the string to a single entry list.
    Otherwise, assume it's an iterable dir, list or tuple of
    strings.

    Args:
        input_array: The object to test
    Returns:
        The input, or a string encapsulated into a single entry list.

    """

    # If empty, return an empty array
    if input_array is None:
        input_array = []
    elif is_string(input_array):
        # Convert a single entry into an array
        input_array = [input_array]
    return input_array

########################################


def TrueFalse(item):            # pylint: disable=C0103
    """
    Convert the input into a boolean and return the string 'True' or 'False'

    If the input was a string of '0' or 'False' (Case insensitive comparision),
    this function will return 'False'. Empty dictionary, string or list objects,
    or the number zero will also return 'False'

    Args:
        item: Object to convert to a bool before converting into a string
    Returns:
        The string 'True' or 'False'
    See:
        truefalse() or TRUEFALSE()
    """

    # Test if it's the string 'False'
    if is_string(item):
        if item == '0' or item.upper() == 'FALSE':
            return 'False'
    if bool(item):
        return 'True'
    return 'False'

########################################


def truefalse(item):
    """
    Convert the input into a boolean and return the string 'true' or 'false'

    If the input was a string of '0' or 'False' (Case insensitive comparision),
    this function will return 'false'. Empty dictionary, string or list objects,
    or the number zero will also return 'false'

    Args:
        item: Object to convert to a bool before converting into a string
    Returns:
        The string 'true' or 'false'
    See:
        TRUEFALSE() or TrueFalse()
    """

    # Test if it's the string 'False'
    if is_string(item):
        if item == '0' or item.upper() == 'FALSE':
            return 'false'
    if bool(item):
        return 'true'
    return 'false'

########################################


def TRUEFALSE(item):            # pylint: disable=C0103
    """
    Convert the input into a boolean and return the string 'TRUE' or 'FALSE'

    If the input was a string of '0' or 'False' (Case insensitive comparision),
    this function will return 'FALSE'. Empty dictionary, string or list objects,
    or the number zero will also return 'FALSE'

    Args:
        item: Object to convert to a bool before converting into a string
    Returns:
        The string 'TRUE' or 'FALSE'
    See:
        truefalse() or TrueFalse()
    """

    # Test if it's the string 'False'
    if is_string(item):
        if item == '0' or item.upper() == 'FALSE':
            return 'FALSE'
    if bool(item):
        return 'TRUE'
    return 'FALSE'

########################################


def convert_to_windows_slashes(path_name, force_ending_slash=False):
    """
    Convert a filename from Linux/macOS to Windows format

    Convert all '/' characters into '\' characters

    If force_ending_slash is True, append a '\' if one is not
    present in the final string

    Args:
        path_name: A pathname to be converted to Windows slashes
        force_ending_slash: True if a '\\' character is to be forced at the end
            of the output

    Returns:
        A pathname using Windows type slashes '\\'

    See:
        convert_to_linux_slashes()

    """

    result = path_name.replace('/', '\\')
    if force_ending_slash and not result.endswith('\\'):
        result = result + '\\'
    return result

########################################


def convert_to_linux_slashes(path_name, force_ending_slash=False):
    """
    Convert a filename from Windows to Linux/macOS format

    Convert all '\' characters into '/' characters

    Args:
        path_name: A string object that text substitution will occur
        force_ending_slash: True if a '/' character is to be forced at the end
            of the output

    Returns:
        A pathname using Linux/BSD type slashes '/'

    See:
        convert_to_windows_slashes()
    """

    result = path_name.replace('\\', '/')
    if force_ending_slash and not result.endswith('/'):
        result = result + '/'
    return result

########################################


def encapsulate_path_windows(input_path):

    """
    Quote a pathname for use in the Windows system shell

    On Windows platforms, if the path has a space or other
    character that could confuse COMMAND.COM, the string
    will be quoted and double quotes within the string handled
    properly. All slash characters will be replaced with backslash
    characters.

    Args:
        input_path: string with the path to encapsulate using Windows rules
    Returns:
        Original input string if Windows can accept it or input properly quoted
    See:
        encapsulate_path()
    """

    # Force to Windows slashes
    temp = convert_to_windows_slashes(input_path)

    # If there are any illegal characters, break
    for item in temp:
        if item not in _WINDOWSSAFESET:
            break
    else:
        # No illegal characters in the string
        if not temp:
            return '""'
        return temp

    # Since the test failed, quote the string
    return '"{}"'.format(temp.replace('"', '\\"'))

########################################


def encapsulate_path_linux(input_path):
    """
    Quote a pathname for use in the linux or BSD system shell

    On Linux platforms, if the path has a space or other
    character that could confuse bash, the string
    will be quoted and double quotes within the string handled
    properly. All backslash characters will be replaced with slash
    characters.

    Args:
        input_path: string with the path to encapsulate using Windows rules
    Returns:
        Original input string if Windows can accept it or input properly quoted
    See:
        encapsulate_path()
    """

    # Force to linux slashes
    temp = convert_to_linux_slashes(input_path)

    # If there are illegal characters for linux/BSD, break
    for item in temp:
        if item not in _LINUXSAFESET:
            break
    else:
        # String doesn't need quotes
        if not temp:
            return "''"
        return temp
    # Enquote the string for Linux or MacOSX
    return "'{}'".format(temp.replace("'", "'\"'\"'"))

########################################


def encapsulate_path(input_path):

    """
    Quote a pathname for use in the native system shell

    On Windows platforms, if the path has a space or other
    character that could confuse COMMAND.COM, the string
    will be quoted, and for other platforms, it will
    be quoted using rules that work best for BASH.
    This will also quote if the path has a ';' which
    could be used to confuse bash.

    Args:
        input_path: string with the path to encapsulate
    Returns:
        Input string or input properly quoted
    See:
        encapsulate_path_windows() and encapsulate_path_linux()
    """

    # Process for Windows platforms
    if os.name == 'nt':
        return encapsulate_path_windows(input_path)

    # Force to linux slashes
    return encapsulate_path_linux(input_path)

########################################


def split_comma_with_quotes(comma_string):
    """
    Split comma seperated string while handling quotes

    str.split(',') will split a string into a list but it doesn't
    handle entries that are encased in quotes. This function will
    scan for quote characters and skip over any comma that's encased
    in quotes.

    Examples:
        # Result is ['"foo,bar"','foo','bar']
        lines = burger.strutils.split_comma_with_quotes('"foo,bar",foo,bar')

        # Will raise an error due to missing end quote
        willraise = burger.strutils.split_comma_with_quote('"foo,bar')

    Args:
        comma_string: String of comma seperated strings

    Return:
        List of string fragments for each comma seperated entries

    Raises:
        ValueError

    """

    # Start the parsing at the start of the string
    marker = 0

    # No delimiter found yet
    delimiter = ''
    result = []

    # Get list of matches
    for match in _RE_COMMA_QUOTES.finditer(comma_string):

        # Get the character that matched
        temp = match.group(0)

        # Looking for a comma?
        if delimiter == '':

            # This is a comma that will trigger a split
            if temp == ',':

                # Add in the string that was found
                result.append(comma_string[marker:match.start()])
                # Mark AFTER the comma with +1
                marker = match.start() + 1

            # Is this a quote?
            elif temp in "\"'":
                # Mark a delimiter and stop checking for commas
                delimiter = temp

        # A delimiter is being tracker, has it been hit?
        elif temp == delimiter:
            # Enable splitting on commas
            delimiter = ''

    # If the quote was not matched, throw an exception
    if delimiter:
        raise ValueError("String wasn't properly quoted")

    # Grab the last string chunk
    temp = comma_string[marker:]

    # If it's empty, it's because there was a trailing comma
    if temp:
        result.append(temp)

    return result

########################################


def parse_csv(csv_string):
    """
    Parse a comma seperated string allowing quoted strings

    Given a string of comma seperated entries and handle quotes properly.

    Examples:
        # Result is ['foo,bar','foo','bar']
        lines = burger.strutils.split_comma_with_quotes('"foo,bar",foo,bar')

        # Result is ['foo"bar',"'boo'boo'"]
        lines = burger.strutils.split_comma_with_quotes('"foo""bar","'boo,boo'")

        # Will raise an error due to missing end quote
        willraise = burger.strutils.split_comma_with_quote('"foo,bar')

    Args:
        csv_string: String of comma seperated entries
    Returns:
        List of entries with whitespace stripped from prefix and suffix
    Raises:
        ValueError
    """

    result = []
    # Seperate by commas
    for item in split_comma_with_quotes(csv_string):

        # Strip the whitespace
        temp = item.strip('\n\r \t')

        # Is there anything to parse?
        if temp:

            # Check if there's a delimiter
            delimiter = temp[0]
            if delimiter in "\"'":

                # If there's a delimiter, properly handle it
                temp = next(csv.reader([temp], quotechar=str(delimiter), \
                    delimiter=str(delimiter), quoting=csv.QUOTE_ALL))[0]
        result.append(temp)
    return result

########################################


def translate_to_regex_match(file_list):
    """
    Translate filename wildcards into regexes

    Args:
        file_list: List of filename wildcards
    Results:
        List of re.compile().match entries
    """

    # Start with an empty folder
    result = []
    for item in file_list:
        # Translate and then return the match function
        result.append(re.compile(fnmatch.translate(item)).match)
    return result

########################################


def host_machine():
    """
    Return the high level operating system's name

    Return the machine this script is running on, 'windows', 'macosx',
    'linux' or 'unknown'

    Returns:
        The string 'windows', 'macosx', 'linux', or 'unknown'

    See:
        get_mac_host_type() or get_windows_host_type()
    """
    # Only windows reports as NT

    if os.name == 'nt':
        return 'windows'

    # BSD and GNU report as posix

    if os.name == 'posix':

        # MacOSX is the Darwin kernel

        if platform.system() == 'Darwin':
            return 'macosx'

        # Assume linux (Tested on Ubuntu and Red Hat)

        return 'linux'

    # Surrender Dorothy

    return 'unknown'

########################################


def get_windows_host_type():
    """
    Return windows host type (32 or 64 bit)

    Return False if the host is not Windows, 'x86' if it's a 32 bit host
    and 'x64' if it's a 64 bit host, and possibly 'arm' if an arm host

    Returns:
        The string 'x64', 'x86', 'arm' or False
    See:
        get_mac_host_type() or host_machine()

    """

    # Not windows?

    if os.name != 'nt':
        return False

    # Test the CPU for the type

    machine = platform.machine()
    if machine in ('AMD64', 'x86_64'):
        return 'x64'
    return 'x86'

########################################


def get_mac_host_type():
    """
    Return Mac OSX host type (PowerPC/Intel)

    Return False if the host is not Mac OSX. 'ppc' if it's a Power PC based
    system, 'x86' for Intel (Both 32 and 64 bit)

    Returns:
        The string 'x86', 'ppc' or False

    See:
        get_windows_host_type() or host_machine()
    """

    # Mac/Linux?
    if os.name != 'posix':
        return False

    # Not linux?

    if platform.system() != 'Darwin':
        return False

    # Since it's a mac, query the Mac OSX cpu type
    # using the MacOSX python extensions

    cpu = platform.machine()
    if cpu in ('x86', 'x86_64'):
        return 'x86'

    if cpu in ('PowerPC', 'ppc', 'Power Macintosh'):
        return 'ppc'

    # Defaults to PowerPC
    return 'ppc'
