#!/usr/bin/env python

from __future__ import print_function
import sys
import os
import argparse
import datetime
import yaml
import em
import tempfile
import shutil
import subprocess
import glob
import itertools

try: input = raw_input
except: pass

GENER8_FILE_NAME = 'data.gener8'
TEMPLATE_CONFIG_FILE_NAME = 'config.gener8'
TEMPLATE_DEFAULT_DATA_FILE_NAME = 'default.gener8'

def get_template_dir():
    template_dir = os.environ.get('GENER8_DIR')

    if not template_dir:
        template_dir = os.path.expanduser("~/.gener8")

    return template_dir

def list_templates(template_dir):

    if template_dir is None:
        template_dir = os.path.dirname(os.path.realpath(__file__))

    try:
        template_list = next(os.walk(template_dir))[1]
    except StopIteration:
        print('Template directory not found at \"{}\"'.format(template_dir))
        return

    template_list[:] = [d for d in template_list if not d[0] == '.']

    if not template_list:
        print('No template found in \"{}\" directory'.format(template_dir))
        exit()

    print('\n  Template list:\n')

    template_list.sort()
    for template in template_list:
        print('    - {}'.format(os.path.basename(template)))

    print('')

def get_target_dir(templates, target_dir= None, request_dir=None):

    if target_dir is None:

        def default_request_dir():
            return input(
                "Please provide a destination [{}]: ".format(
                    default_dir))

        if request_dir is None:
            request_dir = default_request_dir

        default_dir = "{0}-{1}".format('_'.join(templates), datetime.date.today())
        target_dir = request_dir()

        if not target_dir:
            target_dir = default_dir

    return os.path.abspath(target_dir)

def parse_args(args):

    parser = argparse.ArgumentParser(description=(
        'The script will successively copy content from \"templates\" folders to the destination folder called \"DEST\". '
        'You can choose from where templates are found by setting the "GENER8_DIR" environment variable to a valid path. '
        'The current template directory is: \"{}\"').format(get_template_dir()))

    parser.add_argument("-l", "--list", help="list available templates and exit", action="store_true")
    #parser.add_argument("-f", "--force", help="force overwrite of existing files", action="store_true")
    parser.add_argument("-d", "--dest", help="destination directory path")
    parser.add_argument('templates', nargs='*')

    return parser.parse_args(args)

def get_source_dir(template_name):

    template_dir = get_template_dir()
    source_dir = os.path.join(template_dir, template_name)

    if not os.path.isdir(source_dir):
        print("Template \"{0}\" not found in \"{1}\" directory, choose one from:".format(
            template_name, template_dir))
        list_templates(template_dir)
        exit()

    if '@' in source_dir:
        raise EnvironmentError((
            '\'@\' found in template directory: {}\n').format(source_dir))

    return source_dir

def parse_file(file_path):

    with open(file_path, 'r') as file_stream:
        data = yaml.load(file_stream)

    if data is None:
        data = {}

    return data

def walk_dict(d, history=[]):

    for key, value in list(d.items()):
        if isinstance(value, dict):
            history.append(key)
            for k, v in walk_dict(value, history):
                yield k, v
            history.remove(key)
        else:
            yield history + [key], value

def nested_set(d, keys, value):
    for key in keys[:-1]:
        d = d.setdefault(key, {})
    d[keys[-1]] = value

def request_data(default_data):

    use_default = input('Do you want to use default values (y/n) [y] ? ')

    data = default_data

    if use_default == 'n':
        for keys, default_value in walk_dict(data):
            if default_value is None: default_value = ''
            value = input('{0} [{1}]: '.format('.'.join(keys), default_value.encode('utf8')))
            if value: nested_set(data, keys, value)

    return data

def dump_data(data):

    save = input('Do you want to save values (y/n) [n] ? ')

    if save == 'y':
        with open(GENER8_FILE_NAME, 'wb+') as outfile:
            outfile.write(yaml.safe_dump(
                data,
                default_flow_style=False,
                encoding='utf-8',
                allow_unicode=True))

def format_data(data):

    class DictAsMember(dict):
        def __getattr__(self, name):
            value = self[name]

            try:
                if isinstance(value, dict):
                    value = DictAsMember(value)
                elif isinstance(value, unicode):
                    data[key] = value.encode('utf8')
            except NameError:
                pass

            return value

    for key, value in list(data.items()):

        try:
            if isinstance(value, dict):
                data[key] = DictAsMember(value)
            elif isinstance(value, unicode):
                data[key] = value.encode('utf8')
        except NameError:
            pass

def get_data(source_dir, target_dir):

    data = {}

    default_data_path = os.path.join(source_dir, TEMPLATE_DEFAULT_DATA_FILE_NAME)

    try:
        default_data = parse_file(default_data_path)
    except IOError:
        print('No default data file provided in template \"{}\"'.format(source_dir))
        default_data = {}
    else:

        user_data_path = os.path.join(os.getcwd(), GENER8_FILE_NAME)

        try:
            user_data = parse_file(user_data_path)
            print('Using \"{}\" for providing expansion data...'.format(user_data_path))
        except IOError:
            user_data = request_data(default_data)
            dump_data(user_data)

        data = dict(default_data, **user_data)

    format_data(data)

    return data

def get_config(source_dir):

    config = {}

    config_path = os.path.join(source_dir, TEMPLATE_CONFIG_FILE_NAME)

    try:
        config = parse_file(config_path)
    except IOError:
        print('No config file provided in template \"{}\"'.format(source_dir))

    return config

def apply_parents(parents, target_dir, children):

    for parent in parents:
        if parent not in children:
            apply_template(parent, target_dir, children)
        else:
            print(
                'Recursive template inheritance detected: {0} -> {1}'.format(
                    parent, children))

def copy_tree(src, dst, data={}, exclude=[]):

    copied_files = []

    excluded_files = [
        os.path.join(src, TEMPLATE_CONFIG_FILE_NAME),
        os.path.join(src, TEMPLATE_DEFAULT_DATA_FILE_NAME),]

    for exclude_pattern in exclude:
        paths = expanded_paths(exclude_pattern, src)
        excluded_files += paths

    if not os.path.isdir(dst):
        os.mkdir(dst)
    for item in os.listdir(src):
        s = os.path.join(src, item)
        d = os.path.join(dst, em.expand(item, data))
        if os.path.isdir(s):
            copied_files += copy_tree(s, d, data)
        elif s not in excluded_files:
            if os.path.exists(d):
                print('\"{}\" already exists: no overwriting'.format(d))
            else:
                shutil.copy2(s, d)
                copied_files.append(d)

    return copied_files

def apply_data_to_template_file(template_file, data):

    output_stream = tempfile.NamedTemporaryFile(mode='r+', delete=False)
    interpreter = em.Interpreter(output=output_stream, globals=dict(data), options={em.BANGPATH_OPT: False})

    try:
        interpreter.file(open(template_file))
    except em.ParseError as e:
        print('Parse error in {0}: {1}'.format(template_file, str(e)))
    except NameError as e:
        print('Unknown data in {0}: {1}'.format(template_file, str(e)))
    finally:
        interpreter.shutdown()

    shutil.move(output_stream.name, template_file)

def expanded_paths(path, root = ''):

    try:
        base_path, ext = path.split('**/', 1)
    except ValueError:
        results = glob.iglob(os.path.join(root, path))
    else:
        root = os.path.normpath(os.path.join(root, base_path))
        results = itertools.chain.from_iterable(glob.iglob(os.path.join(new_root, ext))
                                            for new_root, dirs, files in os.walk(root))

    return [result for result in results if not os.path.isdir(result)]

def apply_data_to_template_files(template_file_paths, file_paths, data, dest_dir):

    for template_file_path in template_file_paths:
        for expanded_template_file_path in expanded_paths(template_file_path, dest_dir):
            abs_expanded_template_file_path = os.path.abspath(expanded_template_file_path)
            if abs_expanded_template_file_path in file_paths:
                apply_data_to_template_file(abs_expanded_template_file_path, data)

def apply_commands(shell, commands, dest_dir):

    output = []

    for command in commands.split('\n'):

        if command:

            process = subprocess.Popen(
                shell.split(' '),
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                cwd=dest_dir)

            (command_output, errors) = process.communicate(commands)
            print(command_output, end='')
            print(errors, file=sys.stderr, end='')
            output.append(command_output)

    return output

def apply_multi_shells_commands_list(shells_commands_list, dest_dir):

    for shells_commands in shells_commands_list:
        for shell, commands in list(shells_commands.items()):
            apply_commands(shell, commands, dest_dir)

def get_from_dict(dict, key, default):

    try:
        value = dict[key]
    except KeyError:
        value = default

    return value

def apply_template(template, dest_dir, children=[]):

    children.append(template)

    source_dir = get_source_dir(template)

    data = get_data(source_dir, dest_dir)
    config = get_config(source_dir)

    pre = get_from_dict(config, 'pre', [])
    apply_multi_shells_commands_list(pre, dest_dir)

    parents = get_from_dict(config, 'parents', [])
    apply_parents(parents, dest_dir, children)

    print("Copying content from \"{0}\" directory to \"{1}\"...".format(
        source_dir, dest_dir))

    exclude = get_from_dict(config, 'exclude', [])

    copied_files = copy_tree(source_dir, dest_dir, data, exclude)

    parse = get_from_dict(config, 'parse', [])
    apply_data_to_template_files(parse, copied_files, data, dest_dir)

    post = get_from_dict(config, 'post', [])
    apply_multi_shells_commands_list(post, dest_dir)

    children.remove(template)

def apply_templates(templates, target_dir):

    for template in templates:
        apply_template(template, target_dir)

if __name__ == '__main__':

    try:
        try:
            args = sys.argv[1:]
        except IndexError:
            args = []

        args = parse_args(args)

        if args.list:
            list_templates(get_template_dir())
            exit()

        if not args.templates:
            print('No template provided: nothing to do')
            exit()

        target_dir = get_target_dir(args.templates, args.dest)

        apply_templates(args.templates, target_dir)

    except KeyboardInterrupt:
        print('\ngener8 aborted')
