# -*- coding:utf-8 -*-

from io import open

import os
import os.path
import stat
from copy import deepcopy, copy
import shutil
import re

import importlib.util

from glob import glob

import logging
logger = logging.getLogger(__name__)


__all__ = [
    'list_files',
    'extract_filename',
    'dict_from_path',
    'deep_merge_dict',
    'underscore_var_names',
    'add_url_path_component',
    'copy_tree',
    'calculate_association_table_name',
    'get_url_file_ext',
    'generate_quickstart',
    'import_python_modules_by_path',
    'get_project_config_file',
    'dict_strip',
    'strip_str',
    'strip_el_text',
    'find_first_file_with_ext',
    'uncapitalize',
    'find_duplicates_in_array',
    'camel_to_snake',
]

DEFAULT_CONFIG_CONTENT = """project-name: Your project name
base-path: /
"""


def list_files(base_path, ext=None):
    """Lists all of the files in the given base directory, optionally only
    including whose extension(s) match the ext string/list of strings.
    This is non-recursive.

    Args:
        base_path: The directory in which to search.
        ext: The extension(s) to match in the given directory. If None, this
            matches all file extensions.

    Returns:
        A list of filenames relative to the given base path.
    """
    if not os.path.isdir(base_path):
        raise ValueError("Path does not exist: %s" % base_path)

    files = []
    for entry in os.listdir(base_path):
        if os.path.isfile(os.path.join(base_path, entry)):
            _, entry_ext = os.path.splitext(entry)
            entry_ext = entry_ext.lstrip('.')

            if (ext is None) or \
                (isinstance(ext, str) and entry_ext == ext) or \
                (isinstance(ext, list) and entry_ext in ext):
                files.append(entry)

    return files


def extract_filename(path):
    return list(os.path.splitext(os.path.basename(path)))[0]


def dict_from_path(path, final_value=dict()):
    components = path.split('/')
    last_dict = deepcopy(final_value)
    cur_dict = {}
    for i in range(-1, -len(components)-1, -1):
        if len(components[i]) > 0:
            cur_dict = {components[i]: last_dict}
            last_dict = deepcopy(cur_dict)

    return cur_dict


def deep_merge_dict(a, b):
    """Deep merges dictionary b into dictionary a."""
    if not isinstance(a, dict):
        raise TypeError("a must be a dict, but found %s" % a.__class__.__name__)
    if not isinstance(b, dict):
        raise TypeError("b must be a dict, but found %s" % b.__class__.__name__)

    _a = copy(a)
    _b = copy(b)

    for key_b, val_b in _b.items():
        # if it's a sub-dictionary
        if isinstance(val_b, dict):
            if key_b not in _a or not isinstance(_a[key_b], dict):
                _a[key_b] = {}

            # perform the deep merge recursively
            _a[key_b] = deep_merge_dict(_a[key_b], val_b)
        else:
            _a[key_b] = val_b

    # b should now be deep-merged into a
    return _a


def underscore_var_names(d):
    _d = {}
    for k, v in d.items():
        _k = k.replace('-', '_')
        # perform the underscoring recursively
        _d[_k] = underscore_var_names(v) if isinstance(v, dict) else v

    return _d


def add_url_path_component(path, component):
    return '%s/%s' % (path.rstrip('/'), component.lstrip('/'))


def copy_file_if_modified(src_path, dest_path):
    """Only copies the file from the source path to the destination path if it doesn't exist yet or it has
    been modified. Intended to provide something of an optimisation when a project has large trees of assets."""

    # if the destination path is a directory, delete it completely - we assume here we are
    # writing a file to the filesystem
    if os.path.isdir(dest_path):
        shutil.rmtree(dest_path)

    must_copy = False
    if not os.path.exists(dest_path):
        must_copy = True
    else:
        src_stat = os.stat(src_path)
        dest_stat = os.stat(dest_path)

        # if the size or last modified timestamp are different
        if ((src_stat[stat.ST_SIZE] != dest_stat[stat.ST_SIZE]) or
                (src_stat[stat.ST_MTIME] != dest_stat[stat.ST_MTIME])):
            must_copy = True

    if must_copy:
        shutil.copy2(src_path, dest_path)


def copy_tree(src_path, dest_path):
    """Copies the entire folder tree, recursively, from the given source path
    to the given destination path. If the destination path does not exist, it
    will be created. If it does, any files/folders within it will be
    overwritten, but none will be deleted."""
    files_copied = 0
    if os.path.isdir(src_path):
        # if the destination folder doesn't exist, create it
        if not os.path.isdir(dest_path):
            os.makedirs(dest_path)

        for entry in os.listdir(src_path):
            src_entry_path = os.path.join(src_path, entry)
            dest_entry_path = os.path.join(dest_path, entry)
            # if it's a sub-folder
            if os.path.isdir(src_entry_path):
                # copy its contents recursively
                files_copied += copy_tree(src_entry_path, dest_entry_path)
            else:
                copy_file_if_modified(src_entry_path, dest_entry_path)
                files_copied += 1

    return files_copied


def calculate_association_table_name(model1_name, model2_name):
    return '%s%s' % (tuple(sorted([model1_name, model2_name])))


def get_url_file_ext(url):
    """Attempts to extract the file extension from the given URL."""
    # get the last part of the path component
    filename = url.split('/')[-1]
    name, ext = os.path.splitext(filename)

    # handle case of files with leading dot
    if not ext and name and name[0] == '.':
        ext = name

    return ext


def generate_quickstart(project_path):
    """Generates all of the basic paths for a Statik project within the given project path. If the project path
     doesn't exist, it will be created."""
    ensure_path_exists(project_path)
    ensure_file_exists(os.path.join(project_path, "config.yml"), DEFAULT_CONFIG_CONTENT)
    ensure_path_exists(os.path.join(project_path, 'models'))
    ensure_path_exists(os.path.join(project_path, 'data'))
    ensure_path_exists(os.path.join(project_path, 'themes'))
    ensure_path_exists(os.path.join(project_path, 'templates'))
    ensure_path_exists(os.path.join(project_path, 'templatetags'))
    ensure_path_exists(os.path.join(project_path, 'views'))
    ensure_path_exists(os.path.join(project_path, 'assets'))


def ensure_path_exists(path):
    if not os.path.isdir(path):
        logger.info('Creating path: %s', path)
        os.makedirs(path)


def ensure_file_exists(path, default_content):
    if not os.path.isfile(path):
        logger.info('Creating file: %s' % path)
        with open(path, 'wt') as f:
            f.write(default_content)


def import_module(module_name, path):
    spec = importlib.util.spec_from_file_location(module_name, path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)


def import_python_modules_by_path(path):
    module_files = glob(os.path.join(path, "*.py"))
    for filename in module_files:
        name = extract_filename(filename)
        import_module(name, filename)


def get_project_config_file(path, default_config_file_name):
    """Attempts to extract the project config file's absolute path from the given path. If the path is a
    directory, it automatically assumes a "config.yml" file will be in that directory. If the path is to
    a .yml file, it assumes that that is the root configuration file for the project."""
    _path, _config_file_path = None, None
    path = os.path.abspath(path)

    if os.path.isdir(path):
        _path = path
        # use the default config file
        _config_file_path = os.path.join(_path, default_config_file_name)
        logger.debug("Using default project configuration file path: %s", _config_file_path)
    elif path.endswith(".yml"):
        _path = os.path.dirname(path)
        _config_file_path = path
        logger.debug("Using custom project configuration file path: %s", _config_file_path)

    return _path, _config_file_path


def dict_strip(d):
    """Strips whitespace from the string values of the given dictionary (recursively).

    Args:
        d: A dictionary object.

    Returns:
        A new dictionary object, whose string values' whitespace has been stripped out.
    """
    _d = deepcopy(d)
    for k, v in d.items():
        if isinstance(v, str):
            _d[k] = v.strip()
        elif isinstance(v, dict):
            _d[k] = dict_strip(v)

    return _d


def strip_str(s):
    """Strips newlines and whitespace from the given string."""
    return ' '.join([w.strip() for w in s.strip().split('\n')])


def strip_el_text(el, max_depth=0, cur_depth=0):
    """Recursively strips the plain text out of the given XML etree element up to the desired depth.

    Args:
        el: The etree element to scan.
        max_depth: The depth to which to recursively strip text (default: 0).
        cur_depth: The current recursive depth to which we've scanned so far.

    Returns:
        The stripped, plain text from within the element.
    """
    # text in front of any child elements
    el_text = strip_str(el.text if el.text is not None else "")

    if cur_depth < max_depth:
        for child in el:
            el_text += " "+strip_el_text(child, max_depth=max_depth, cur_depth=cur_depth+1)
    else:
        # add the last child's tail text, if any
        children = list(el)
        if children is not None and len(children) > 0:
            if children[-1].tail is not None:
                el_text += " "+strip_str(children[-1].tail)

    # we skip the root element
    if cur_depth > 0:
        # in case there's any text at the end of the element
        if el.tail is not None:
            el_text += " "+strip_str(el.tail)

    return strip_str(el_text)


def find_first_file_with_ext(base_paths, prefix, exts):
    """Runs through the given list of file extensions and returns the first file with the given base
    path and extension combination that actually exists.

    Args:
        base_paths: The base paths in which to search for files.
        prefix: The filename prefix of the file for which to search.
        exts: An ordered list of file extensions for which to search.

    Returns:
        On success, a 2-tuple containing the base path in which the file was found, and the extension of the file.
        On failure, returns (None, None).
    """
    for base_path in base_paths:
        for ext in exts:
            filename = os.path.join(base_path, "%s%s" % (prefix, ext))
            if os.path.exists(filename) and os.path.isfile(filename):
                logger.debug("Found first file with relevant extension: %s", filename)
                return base_path, ext

    logger.debug("No files found for prefix %s, extensions %s", prefix, ", ".join(exts))
    return None, None


def uncapitalize(s):
    """If the given string begins with a capital letter, it converts it to lowercase."""
    return (s[:1].lower() + s[1:]) if s else ""


def find_duplicates_in_array(array):
    """Runs through the array and returns the elements that contain
    more than one duplicate

    Args:
        array: The array to check for duplicates.

    Returns:
        Array of the elements that are duplicates. Returns empty list if
        there are no duplicates.
    """
    duplicates = []
    non_duplicates = []

    if len(array) != len(set(array)):
        for item in array:
            if item not in non_duplicates:
                non_duplicates.append(item)
            elif item in non_duplicates and item not in duplicates:
                duplicates.append(item)

    return duplicates

def camel_to_snake(camel):
    return '_'.join(re.findall(r'[A-Z][a-z]*', camel))
