# encoding: utf-8
import threading
import time
import os
import platform
import sys
import errno
import string
import random
import codecs
from contextlib import closing
from contextlib import contextmanager
from functools import wraps
from collections import defaultdict, OrderedDict, namedtuple
from subprocess import Popen

from parallels.core import messages
from plesk_migrator.compatibility.utils import stringio


def if_not_none(value, func):
    return func(value) if value is not None else None


def is_not_none(value, func):
    return bool(func(value)) if value is not None else False


def is_empty_iterator(iterator):
    for _ in iterator:
        return False
    return True


class Obj:
    def __init__(self, kw):
        self.__dict__.update(kw)

    def __repr__(self):
        # TODO: add unicode support, enquote string values in representation
        return "obj(" + ", ".join(["%s=%s" % (k, v) for k, v in self.__dict__.items()]) + ")"


def obj(**kw):
    return Obj(kw)


def parse_bool(raw_value, default_value=None):
    if raw_value in ('true', '1'):
        return True
    elif raw_value in ('false', '0'):
        return False
    else:
        if default_value is None:
            raise Exception(messages.INVALID_BOOLEAN_VALUE % (raw_value,))
        else:
            return bool(default_value)


utf8_tree_symbols = {'|': u'│ ', '|-': u'├ ', '`-': u'└ ', ' ': '  '}
ascii_tree_symbols = {'|': u'|  ', '|-': u'|- ', '`-': u'`- ', ' ': '   '}


def draw_tree(root, symbols, spacing=0):
    def draw(node, indent, child_indent, stream):
        name, children = node
    
        lines = name.split('\n')
        stream.write(u'%s%s\n' % (indent, lines[0]))
        
        for line in lines[1:]:
            stream.write(u'%s%s\n' % (child_indent, line))
    
        if children:
            spacing_line = u'%s\n' % ('\n'.join([child_indent+symbols['|']] * spacing))
            if spacing > 0: 
                stream.write(spacing_line)
            for child in children[:-1]:
                draw(child, child_indent+symbols['|-'], child_indent+symbols['|'], stream)
                if spacing > 0: 
                    stream.write(spacing_line)
            draw(children[-1], child_indent+symbols['`-'], child_indent+symbols[' '], stream)
    
    with closing(stringio(is_unicode=True)) as stream_with_tree:
        draw(root, u'', u'', stream_with_tree)
        return stream_with_tree.getvalue()


def format_table(rows, h_padding=2):
    """Format 2-dimension array into nice text table, which could be printed to log or console

    :type rows: list[list]
    :type h_padding: int
    :rtype: str | unicode
    """
    if len(rows) == 0:
        return ''

    cols = defaultdict(list)

    for row in rows:
        for col_num, val in enumerate(row):
            cols[col_num].append(val)

    max_col_length = {
        col_num: max(
            len(val_line)
            for val in col
            for val_line in safe_string_repr(val, omit_quotes=True).split("\n")
        )
        for col_num, col in cols.items()
    }

    border_row = "+%s+\n" % (
        '+'.join([
            '-' * (max_len + h_padding)
            for max_len in max_col_length.values()
        ])
    )

    result = ''
    result += border_row

    row_strings = []
    for row in rows:
        row_string = ""
        row_lines_count = max(len(safe_string_repr(val, omit_quotes=True).split("\n")) for val in row)
        if sys.version_info.major >= 3:
            row_lines_count_range = range(row_lines_count)
        else:
            row_lines_count_range = xrange(row_lines_count)
        for line in row_lines_count_range:
            row_string_parts = []
            for col_num, value in enumerate(row):
                value_lines = safe_string_repr(value, omit_quotes=True).split("\n")
                if line < len(value_lines):
                    value_line = value_lines[line]
                else:
                    value_line = ""
                row_string_parts.append(value_line.center(max_col_length[col_num] + h_padding))
            row_string += "|%s|\n" % ("|".join(row_string_parts))
        row_strings.append(row_string)

    result += border_row.join(row_strings)

    result += border_row

    return result


def generate_random_password(chars_to_include=''):
    """Return a string of random characters.

    Arguments:
    chars_to_include: A list of characters to include. In addition to randomly
    chosen characters, characters from the list will be included in random
    order. The password length will increase accordingly.
    """
    chars = [random.choice(string.lowercase) for _ in range(3)] + \
            [random.choice(string.uppercase) for _ in range(3)] + \
            [random.choice(string.digits) for _ in range(4)] + \
            [c for c in chars_to_include]
    random.shuffle(chars)
    return "".join(chars)


def sort_list_by_another(lst, order_lst):
    """
    Sort one list with the order taken from another list
    """
    return [value for value in order_lst if value in lst]


def ilen(iterable):
    """Get number of elements in iterable collection.
    """
    count = 0
    for _ in iterable:
        count += 1
    return count


def partition(iterable, func):
    condition_true = []
    condition_false = []

    for item in iterable:
        if func(item):
            condition_true.append(item)
        else:
            condition_false.append(item)
    return condition_true, condition_false


def try_until(condition_func, intervals, sleep=time.sleep):
    def func():
        return True if condition_func() else None
    return poll_data(func, intervals, sleep) is not None


def poll_data(func, intervals, sleep=time.sleep):
    value = func()
    if value is not None:
        return value

    for interval in intervals:
        sleep(interval)
        value = func()
        if value is not None:
            return value

    return None


def polling_intervals(starting, max_attempt_interval, max_total_time=None):
    """Generate sequence of times for polling some potentially long-lasting process.
    """
    interval = starting
    total = 0

    while interval < max_attempt_interval and (max_total_time is None or total < max_total_time):
        yield interval
        total += interval
        interval *= 2

    while max_total_time is None or total < max_total_time:
        yield max_attempt_interval
        total += max_attempt_interval


def unused(*args):
    """This function does nothing and is used to prevent code analyzing tools from reporting
    variable or imported module as unused.

    Please describe why this unused variable is needed next to function call.
    """
    args = args


def all_equal(lst):
    return len(lst) == 0 or all(item == lst[0] for item in lst[1:])


def cached(func):
    """Cache function results.
    (cached) call the function func as usual: func(args)
    clear cache for function func as: func.clear()
    """
    cache = {}

    @wraps(func)
    def wrapper(*args, **kw):
        key = (args, frozenset(kw.items()))
        if key in cache:
            return cache[key]
        else:
            value = func(*args, **kw)
            cache[key] = value
            return value

    wrapper.clear = lambda: cache.clear()
    return wrapper


def format_list(lst):
    return ", ".join(["'%s'" % (elem,) for elem in lst])


def format_multiline_list(lst):
    return "\n".join(["- %s" % (elem,) for elem in lst])


def group_by(seq, key_func):
    """
    Example:

    group_by(
        [obj(x=1, y=2), obj(x=3, y=4), obj(x=1, y=5)],
        lambda o: o.x
    )
    {
        1: [obj(x=1, y=2), obj(x=1, y=5)],
        3: [obj(x=3, y=4)]
    }
    """
    d = defaultdict(list)
    for item in seq:
        d[key_func(item)].append(item)
    return dict(d)


def group_by_id(seq, key_func):
    """
    Example:

    group_by(
        [obj(x=1, y=2), obj(x=3, y=4), obj(x=5, y=6)],
        lambda o: o.x
    )
    {
        1: obj(x=1, y=2),
        3: obj(x=3, y=4),
        5: obj(x=5, y=6)
    }
    """
    d = {}
    for item in seq:
        key = key_func(item)
        assert key not in d  # assert that key is really "id" - so it is unique
        d[key] = item
    return d


def group_value_by_id(seq, key_func, value_func):
    """
    Example:

    group_by(
        [obj(x=1, y=2), obj(x=3, y=4), obj(x=5, y=6)],
        lambda o: o.x,
        lambda o: o.y
    )
    {
        1: 2,
        3: 4,
        5: 6
    }
    """
    d = {}
    for item in seq:
        key = key_func(item)
        assert key not in d  # assert that key is really "id" - so it is unique
        d[key] = value_func(item)
    return d


def group_value_by(seq, key_func, value_func):
    """
    Example:

    group_by(
        [obj(x=1, y=2), obj(x=3, y=4), obj(x=3, y=7), obj(x=5, y=6)],
        lambda o: o.x,
        lambda o: o.y
    )
    {
        1: [2],
        3: [4, 7],
        5: [6]
    }
    """
    d = defaultdict(list)
    for item in seq:
        key = key_func(item)
        d[key].append(value_func(item))
    return dict(d)


def mkdir_p(path):
    try:
        os.makedirs(path)
    except OSError as exc:
        if exc.errno == errno.EEXIST:
            pass
        else: 
            raise


def remove_if_exists(file_path):
    """Remove file if it exists.

    :type file_path: str | unicode
    :rtype: None
    """
    if os.path.exists(file_path):
        os.remove(file_path)


def between_inclusive(value, bottom, top):
    return bottom <= value <= top


def between_exclusive(value, bottom, top):
    return bottom < value < top


def find_only(lst, match_func, error_message=messages.FAILED_TO_FIND_ELEMENT_IN_LIST):
    matching_items = [item for item in lst if match_func(item)]
    if len(matching_items) != 1:
        raise Exception(error_message)
    return matching_items[0]


def find_first(lst, match_func, default_value=None):
    for item in lst:
        if match_func(item):
            return item
    return default_value


def exists(lst, match_func):
    for item in lst:
        if match_func(item):
            return True

    return False


def replace_str_prefix(string_with_prefix, old_prefix, new_prefix):
    if string_with_prefix.startswith(old_prefix):
        return new_prefix + string_with_prefix[len(old_prefix):]
    else:
        raise Exception(messages.STRING_EXPECTED_STARTS_WITH % (string_with_prefix, old_prefix))


def split_list_by_count(lst, count):
    sublst = []
    for item in lst:
        sublst.append(item)
        if len(sublst) >= count:
            yield sublst
            sublst = []

    if len(sublst) > 0:  # the last part
        yield sublst


def safe_string_repr(s, omit_quotes=False, encoding=None):
    """If object can be interpreted as unicode, then return it as unicode. Otherwise return __repr__ of its __str__.
    
    Could be useful for safe logging, when usually you want to get just plain string, but if there are some issues
    with encoding - at least get its Python representation.

    :type s: any
    :type omit_quotes: bool
    :type encoding: str
    :rtype: unicode
    """
    if sys.version_info.major >= 3:
        if isinstance(s, str):
            return s
        else:
            try:
                if encoding is not None:
                    s = bytes(s).decode(encoding)

                return str(s)
            except UnicodeError:
                if omit_quotes and isinstance(s, bytes):
                    return (u"%r" % (s,))[1:-1]
                else:
                    return u"%r" % (str(s),)
    else:
        if isinstance(s, unicode):
            return s
        else:
            try:
                if encoding is not None:
                    s = str(s).decode(encoding)

                return unicode(s)
            except UnicodeError:
                if omit_quotes and isinstance(s, basestring):
                    return (u"%r" % (s,))[1:-1]
                else:
                    return u"%r" % (str(s),)


def safe_format(*args, **kwargs):
    """Fault-tolerant string format. Use this function for non-critical formatting, for example logging and reports.

    1) This function allows formatting binary data into user's messages.
    If some argument is a binary string and it can not be converted to unicode -
    it will be inserted as Python representation.
    2) This function makes code more fault-tolerant.
    If some argument is missing for format string, it will be left as is. No exception will be thrown.

    First positional argument is a format string. All the other arguments must be named arguments.

    Output is always an unicode string that could be logged, saved to report file, etc.

    :rtype: unicode
    """
    if len(args) == 0:
        return u''

    safe_kwargs = {
        k: safe_string_repr(v, omit_quotes=True) for k, v in kwargs.items()
    }

    # limit to avoid
    max_missing_keys = 100

    if sys.version_info.major >= 3:
        max_missing_keys_range = range(max_missing_keys)
    else:
        max_missing_keys_range = xrange(max_missing_keys)
    for _ in max_missing_keys_range:
        try:
            if sys.version_info.major >= 3:
                return args[0].format(**safe_kwargs)
            else:
                return unicode(args[0]).format(**safe_kwargs)
        except KeyError as e:
            missing_key = e.args[0]
            safe_kwargs[missing_key] = '{%s}' % missing_key


def strip_multiline_spaces(text):
    """Utility function that strips leading tabs and spaces from multi-line strings.
    Useful when you want to indent multi-line string in a code, but don't want
    to split it over multiple strings for correct indentation
    """
    return u"\n".join([line.strip() for line in text.strip(" \n\t").split("\n")])


def partition_list(the_list, chunk_size=20):
    # borrowed from
    # http://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks-in-python
    if sys.version_info.major >= 3:
        items_range = range(0, len(the_list), chunk_size)
    else:
        items_range = xrange(0, len(the_list), chunk_size)
    for idx in items_range:
        yield the_list[idx: idx + chunk_size]


# Lock to avoid race conditions between "open_no_inherit" and "popen_no_inherit" functions,
# to avoid race condition when the second starts the process before the first one set inherit flag
_no_inherit_lock = threading.Lock()


@contextmanager
def _lock_no_inherit():
    if is_run_on_windows():
        with _no_inherit_lock:
            yield
    else:
        yield


def open_no_inherit(*args, **kwargs):
    """Open files in migrator code. Purpose: avoid issues for Windows multithreading.

    Function works in the same way as "open", accepts the same arguments and returns the same result.

    The issue we try to fix by that function:
    "subprocess: on Windows, unwanted file handles are inherited by child processes in a multithreaded application"
    http://bugs.python.org/issue19575

    Example of an error you could get if you don't use that function:
    "WindowsError: [Error 32] The process cannot access the file because it is being used by another process"

    On Unix works just as plain "open" function.
    """
    with _lock_no_inherit():
        fp = open(*args, **kwargs)

    if is_run_on_windows():
        import msvcrt
        import win32api
        import win32con

        fh = msvcrt.get_osfhandle(fp.fileno())
        win32api.SetHandleInformation(fh, win32con.HANDLE_FLAG_INHERIT, 0)

    return fp


def popen_no_inherit(*args, **kwargs):
    """Call external programs in migrator code. Purpose: avoid issues for Windows multithreading

    Function works in the same way as "subprocess.Popen", accepts the same arguments and returns the same result.

    See "open_no_inherit" for more details.
    """
    with _lock_no_inherit():
        result = Popen(*args, **kwargs)
    return result


def read_file(filename, reader_func, file_may_be_absent=False, default_value=None):
    try:
        with open_no_inherit(filename, 'r') as dump_file:
            return reader_func(dump_file)
    except IOError as e:
        if e.errno == errno.ENOENT and file_may_be_absent:
            return default_value
        else:
            raise


def read_file_contents(filename):
    """Read file contents and return as a string

    :type filename: str | unicode
    :rtype: str
    """
    with open_no_inherit(filename, 'rb') as fp:
        return fp.read()


def read_unicode_file_contents(filename):
    """Read contents of a file in UTF-8 encoding and return as a unicode string

    :type filename: str | unicode
    :rtype: str
    """
    with open_no_inherit(filename, 'rb') as fp:
        return fp.read().decode('utf-8')


def write_unicode_file(filename, contents):
    with open_no_inherit(filename, 'wb') as fp:
        fp.write(contents.encode('utf-8'))


def list_get(lst, index, default_value=None):
    try:
        return lst[index]
    except IndexError:
        return default_value


def fill_none_namedtuple(tuple_class, **kwargs):
    for field in tuple_class._fields:
        if field not in kwargs:
            kwargs[field] = None
    return tuple_class(**kwargs)


def unique_list(lst):
    result = []
    for item in lst:
        if item not in result:
            result.append(item)
    return result


def unique_by(lst, key_func):
    """Make list unique by key, specified by function

    Function takes an item from a list and returns key - some hashable value

    :type lst: list
    :type key_func: function
    :rtype: list
    """
    result = []
    keys = set()
    for item in lst:
        key = key_func(item)
        if key not in keys:
            result.append(item)
            keys.add(key)
    return result


def sorted_dict(dictionary):
    return OrderedDict(sorted(dictionary.items(), key=lambda i: i[0]))


def mutable_namedtuple(name, fields):
    def __init__(self, *args, **kwargs):
        if len(args) > 0:
            for field, arg in zip(fields, args):
                self.__dict__.update({field: arg})
        assert set(list(fields[0: len(args)]) + list(kwargs.keys())) == set(fields), (
            messages.ASSERT_INVALID_ARGUMENTS_PASSED_TO_CONSTRUCTOR
        )
        self.__dict__.update(kwargs)

    def __repr__(self):
        return name + '(' + ', '.join("%s=%r" % (n, getattr(self, n)) for n in fields) + ')'

    def __eq__(self, other):
        for field in fields:
            if not hasattr(other, field):
                return False
            if getattr(self, field) != getattr(other,  field):
                return False
        return True

    def _asdict(self):
        return {field: getattr(self, field) for field in fields}

    return type(name, (object,), {
        '__init__': __init__,
        '__repr__': __repr__,
        '__eq__': __eq__,
        '_asdict': _asdict,
        '_fields': fields
    })


def is_ascii_string(text):
    try:
        text.decode('ascii')
    except UnicodeError:
        return False

    return True


def is_unicode_or_ascii_string(s):
    """Whether specified string is Python unicode string, or Python binary string with ASCII symbols only

    Such string are more safe than Python binary strings with non-ASCII symbols,
    for which unicode issues with formatting usually arise.

    :type s: str | unicode
    :rtype: bool
    """
    if sys.version_info.major >= 3:
        if isinstance(s, str):
            return True
        elif isinstance(s, bytes):
            try:
                s.decode('ascii')
                return True
            except UnicodeError:  # check exception type, UnicodeError may be incorrect
                return False
        else:
            return False
    else:
        if isinstance(s, unicode):
            return True
        elif isinstance(s, str):
            try:
                s.decode('ascii')
                return True
            except UnicodeError:
                return False
        else:
            return False


def is_unicode_or_utf8_string(s):
    """Whether specified string is Python unicode string, or Python binary string which could be decoded with UTF-8

    :type s: str | unicode
    :rtype: bool
    """
    if sys.version_info.major >= 3:
        if isinstance(s, str):
            return True
        elif isinstance(s, bytes):
            try:
                s.decode('utf-8')
                return True
            except UnicodeError:  # check exception type, UnicodeError may be incorrect
                return False
        else:
            return False
    else:
        if isinstance(s, unicode):
            return True
        elif isinstance(s, str):
            try:
                s.decode('utf-8')
                return True
            except UnicodeError:
                return False
        else:
            return False


def extend_namedtuple(base, typename, field_names=None):
    if field_names is None:
        field_names = []
    return namedtuple(typename, list(base._fields) + list(field_names))


def get_executable_file_name():
    return os.path.basename(sys.argv[0])


def is_empty(value):
    return value is None or value == ''


def none_str(value):
    return '' if value is None else value


def str_equals_weak(s1, s2):
    """Compare two strings, ignoring case and leading spaces

    :type s1: str | unicode | None
    :type s2: str | unicode | None
    :rtype: bool
    """
    if is_empty(s1) and is_empty(s2):
        return True
    if is_empty(s1) or is_empty(s2):
        return False

    return s1.strip().lower() == s2.strip().lower()


def all_str_equals_weak(s_list):
    """Compare list of strings, ignoring case and leading spaces

    :type s_list: list[str | unicode]
    :rtype: bool
    """
    if len(s_list) == 0:
        return True

    return all(str_equals_weak(s, s_list[0]) for s in s_list)


def is_empty_list(lst):
    return lst is None or len(lst) == 0


@contextmanager
def open_unicode_file(filename):
    """Open file detecting if it is UTF-8 or UTF-8 with BOM

    See for more details:
    http://stackoverflow.com/questions/13590749/reading-unicode-file-data-with-bom-chars-in-python
    """
    minbytes = min(32, os.path.getsize(filename))
    with open_no_inherit(filename, 'rb') as fp_raw:
        raw = fp_raw.read(minbytes)
        if raw.startswith(codecs.BOM_UTF8):
            encoding = 'utf-8-sig'
        else:
            encoding = 'utf-8'

    with open_no_inherit(filename, 'rb') as fp:
        yield codecs.getreader(encoding)(fp)


def unique_merge(key, lists):
    """Merge several lists making unique by key

    Arguments:
    - key - function which accepts item and returns hashable key
    - lists - list of lists with items
    """
    keys = set()
    result = []
    for lst in lists:
        for item in lst:
            if key(item) not in keys:
                keys.add(key(item))
                result.append(item)

    return result


def merge_dicts(*dicts):
    """Merge several dictionaries, the latest has more priority

    Arguments:
    pass dictionaries as function's arguments, like: merge_dicts(d1, d2, d3)
    """
    result = {}
    for d in dicts:
        for k, v in d.items():
            result[k] = v

    return result


def is_string(o):
    """Check if object is string or not

    :rtype: bool
    """
    if sys.version_info.major >= 3:
        return isinstance(o, bytes)
    else:
        return isinstance(o, basestring)


def is_boolean(o):
    """Check if object is boolean or not

    :rtype: bool
    """
    return o in (True, False)


def is_run_on_windows():
    return platform.system() == 'Windows'


def join_nonempty_strs(strs, delim=' '):
    nonempty_strs = [one_str for one_str in strs if one_str is not None and one_str.strip() != '']
    return delim.join(nonempty_strs)


def split_nonempty_strs(s, separator=None):
    """Split string by lines, return list of trimmed non-empty lines
    :type s: str | unicode
    :type separator: str | unicode | None
    :rtype: list[str]
    """
    return [d.strip() for d in s.split(separator) if d.strip != '']


def default(passed_value, default_value):
    """Shortcut for defining default function arguments

    For example if you have function:
    def f(x):

    and you want 'x' variable to be a list, which is empty by default,
    you can write:

    def f(x=None):
        x = default(x, [])

    instead of

    def f(x=None):
        if x is None:
            x = []

    Please note that you should not use:
    def f(x=[])
    as the default list will be the same object for all function calls.
    So there could be unexpected results if 'x' variable is modified inside a function.
    """
    if passed_value is None:
        return default_value
    else:
        return passed_value


def append_if_not_none(lst, item):
    """Append item to list only if it is not None

    :type lst: list
    :type item: any
    :rtype: None
    """
    if item is not None:
        lst.append(item)


def add_dict_key_prefix(prefix, dictionary):
    """Return a new dictionary, with all key names prefixed by a given string."""
    return dict((prefix+k, v) for k, v in dictionary.items())


def split_string_by_whitespace_chars(s):
    """Split string into list of whitespace and non-whitespace strings

    Each item of resulting list is either whitespace or non-whitespace string, but not mixed one.
    The following constraint is always true:
    s == ''.join(split_string_by_whitespace_chars(s))

    :type s: str | unicode
    :rtype: list[str | unicode]
    """
    parts = []
    previous = None
    current_part = None
    space_chars = {' ', '\t', '\n'}

    for c in s:
        if previous is not None:
            if (c in space_chars) == (previous in space_chars):
                current_part += c
            else:
                parts.append(current_part)
                current_part = c
        else:
            current_part = c

        previous = c

    if current_part != '' and current_part is not None:
        parts.append(current_part)

    return parts


def compare_by(obj1, obj2, attribs):
    """Compare two objects by specified attributes.

    Return True if all specified attributes are equal for these objects, False otherwise.

    :type obj1: object
    :type obj2: object
    :type attribs: list[str]
    :rtype: bool
    """
    return all(getattr(obj1, attr) == getattr(obj2, attr) for attr in attribs)


def remove_empty_lines(s):
    """Remove all empty lines from a string

    :type s: str | unicode
    :rtype: str | unicode
    """
    return b'\n'.join(line for line in s.split(b'\n') if line.strip() != '')


def first_digit(s):
    """Get first digit from specified string

    For example, if specified string is "test567", then 5 is returned

    :type s: str | unicode
    :rtype: int | None
    """
    digits = [c for c in s if c.isdigit()]
    if len(digits) == 0:
        return None
    else:
        return int(digits[0])


def format_bytes(bytes_count):
    """Format bytes count in human-readable form

    :type bytes_count: int
    :rtype: str | unicode
    """
    kb = 1024.0
    mb = 1024.0 * kb
    gb = 1024.0 * mb

    if bytes_count < kb:
        return u"%s bytes" % (bytes_count,)
    elif bytes_count < mb:
        return u"%.2fKB" % (bytes_count / kb)
    elif bytes_count < gb:
        return u"%.2fMB" % (bytes_count / mb)
    else:
        return u"%.2fGB" % (bytes_count / gb)


def format_percentage(numerator, denominator):
    """
    :type numerator: int
    :type denominator: int
    :rtype: str | unicode
    """
    if denominator is None or denominator == 0:
        return 'N/A%'
    else:
        percentage = (float(numerator) / float(denominator)) * 100.0
        return "%.0f%%" % percentage


def append_once(s, suffix):
    """Append suffix to string if it is not appended yet

    :type s: str | unicode
    :type suffix: str | unicode
    ;rtype: str | unicode
    """
    if s.endswith(suffix):
        return s
    else:
        return "%s%s" % (s, suffix)


def strip_suffix(s, suffix):
    """Strip suffix from string. If string does not end with the suffix - return None

    :type s: str | unicode
    :type suffix: str | unicode
    :rtype: str | unicode | None
    """
    if s.endswith(suffix):
        return s[:-len(suffix)]
    else:
        return None


def remove_dict_equal_values(d):
    """Return new dictionary, where all keys which are the same as corresponding values, are removed

    :type d: dict
    :rtype: dict
    """
    return {key: target for key, target in d.items() if key != target}
