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

"""
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

from __future__ import print_function
import itertools
from collections import Counter
import scipy
import numpy as np
import copy
import matplotlib.pyplot as plt
from ..mdr import MDR

def entropy(X, base=2):
    """Calculates the entropy, H(X), in the given base

    Parameters
    ----------
    X: array-like (# samples)
        An array of values for which to compute the entropy
    base: integer (default: 2)
        The base in which to calculate entropy

    Returns
    ----------
    entropy: float
        The entropy calculated according to the equation H(X) = -sum(p_x * log p_x) for all states of X

    """
    return scipy.stats.entropy(list(Counter(X).values()), base=base)

def joint_entropy(X, Y, base=2):
    """Calculates the joint entropy, H(X,Y), in the given base

    Parameters
    ----------
    X: array-like (# samples)
        An array of values for which to compute the joint entropy
    Y: array-like (# samples)
        An array of values for which to compute the joint entropy
    base: integer (default: 2)
        The base in which to calculate joint entropy

    Returns
    ----------
    joint_entropy: float
        The joint entropy calculated according to the equation H(X,Y) = -sum(p_xy * log p_xy) for all combined states of X and Y

    """
    X_Y = ['{}{}'.format(x, y) for x, y in zip(X, Y)]
    return entropy(X_Y, base=base)

def conditional_entropy(X, Y, base=2):
    """Calculates the conditional entropy, H(X|Y), in the given base

    Parameters
    ----------
    X: array-like (# samples)
        An array of values for which to compute the conditional entropy
    Y: array-like (# samples)
        An array of values for which to compute the conditional entropy
    base: integer (default: 2)
        The base in which to calculate conditional entropy

    Returns
    ----------
    conditional_entropy: float
        The conditional entropy calculated according to the equation H(X|Y) = H(X,Y) - H(Y)

    """
    return joint_entropy(X, Y, base=base) - entropy(Y, base=base)

def mutual_information(X, Y, base=2):
    """Calculates the mutual information between two variables, I(X;Y), in the given base

    Parameters
    ----------
    X: array-like (# samples)
        An array of values for which to compute the mutual information
    Y: array-like (# samples)
        An array of values for which to compute the mutual information
    base: integer (default: 2)
        The base in which to calculate mutual information

    Returns
    ----------
    mutual_information: float
        The mutual information calculated according to the equation I(X;Y) = H(Y) - H(Y|X)

    """
    return entropy(Y, base=base) - conditional_entropy(Y, X, base=base)

def two_way_information_gain(X, Y, Z, base=2):
    """Calculates the two-way information gain between three variables, I(X;Y;Z), in the given base

    IG(X;Y;Z) indicates the information gained about variable Z by the joint variable X_Y, after removing
    the information that X and Y have about Z individually. Thus, two-way information gain measures the
    synergistic predictive value of variables X and Y about variable Z.

    Parameters
    ----------
    X: array-like (# samples)
        An array of values for which to compute the 2-way information gain
    Y: array-like (# samples)
        An array of values for which to compute the 2-way information gain
    Z: array-like (# samples)
        An array of outcome values for which to compute the 2-way information gain
    base: integer (default: 2)
        The base in which to calculate 2-way information

    Returns
    ----------
    mutual_information: float
        The information gain calculated according to the equation IG(X;Y;Z) = I(X,Y;Z) - I(X;Z) - I(Y;Z)

    """
    X_Y = ['{}{}'.format(x, y) for x, y in zip(X, Y)]
    return (mutual_information(X_Y, Z, base=base) -
            mutual_information(X, Z, base=base) -
            mutual_information(Y, Z, base=base))

def three_way_information_gain(W, X, Y, Z, base=2):
    """Calculates the three-way information gain between three variables, I(W;X;Y;Z), in the given base

    IG(W;X;Y;Z) indicates the information gained about variable Z by the joint variable W_X_Y, after removing
    the information that W, X, and Y have about Z individually and jointly in pairs. Thus, 3-way information gain
    measures the synergistic predictive value of variables W, X, and Y about variable Z.

    Parameters
    ----------
    W: array-like (# samples)
        An array of values for which to compute the 3-way information gain
    X: array-like (# samples)
        An array of values for which to compute the 3-way information gain
    Y: array-like (# samples)
        An array of values for which to compute the 3-way information gain
    Z: array-like (# samples)
        An array of outcome values for which to compute the 3-way information gain
    base: integer (default: 2)
        The base in which to calculate 3-way information

    Returns
    ----------
    mutual_information: float
        The information gain calculated according to the equation:
            IG(W;X;Y;Z) = I(W,X,Y;Z) - IG(W;X;Z) - IG(W;Y;Z) - IG(X;Y;Z) - I(W;Z) - I(X;Z) - I(Y;Z)

    """
    W_X_Y = ['{}{}{}'.format(w, x, y) for w, x, y in zip(W, X, Y)]
    return (mutual_information(W_X_Y, Z, base=base) -
            two_way_information_gain(W, X, Z, base=base) -
            two_way_information_gain(W, Y, Z, base=base) -
            two_way_information_gain(X, Y, Z, base=base) -
            mutual_information(W, Z, base=base) -
            mutual_information(X, Z, base=base) -
            mutual_information(Y, Z, base=base))

def _mdr_predict(X, Y, labels):
    """Fits a MDR model to variables X and Y with the given labels, then returns the resulting predictions

    This is a convenience method that should only be used internally.

    Parameters
    ----------
    X: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    Y: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    labels: array-like (# samples)
        The class labels corresponding to features X and Y

    Returns
    ----------
    predictions: array-like (# samples)
        The predictions from the fitted MDR model

    """
    return MDR().fit_predict(np.column_stack((X, Y)), labels)

def mdr_entropy(X, Y, labels, base=2):
    """Calculates the MDR entropy, H(XY), in the given base

    MDR entropy is calculated by combining variables X and Y into a single MDR model then calculating
    the entropy of the resulting model's predictions.

    Parameters
    ----------
    X: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    Y: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    labels: array-like (# samples)
        The class labels corresponding to features X and Y
    base: integer (default: 2)
        The base in which to calculate MDR entropy

    Returns
    ----------
    mdr_entropy: float
        The MDR entropy calculated according to the equation H(XY) = -sum(p_xy * log p_xy) for all output states of the MDR model

    """
    return entropy(_mdr_predict(X, Y, labels), base=base)

def mdr_conditional_entropy(X, Y, labels, base=2):
    """Calculates the MDR conditional entropy, H(XY|labels), in the given base

    MDR conditional entropy is calculated by combining variables X and Y into a single MDR model then calculating
    the entropy of the resulting model's predictions conditional on the provided labels.

    Parameters
    ----------
    X: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    Y: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    labels: array-like (# samples)
        The class labels corresponding to features X and Y
    base: integer (default: 2)
        The base in which to calculate MDR conditional entropy

    Returns
    ----------
    mdr_conditional_entropy: float
        The MDR conditional entropy calculated according to the equation H(XY|labels) = H(XY,labels) - H(labels)

    """
    return conditional_entropy(_mdr_predict(X, Y, labels), labels, base=base)

def mdr_mutual_information(X, Y, labels, base=2):
    """Calculates the MDR mutual information, I(XY;labels), in the given base

    MDR mutual information is calculated by combining variables X and Y into a single MDR model then calculating
    the mutual information between the resulting model's predictions and the labels.

    Parameters
    ----------
    X: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    Y: array-like (# samples)
        An array of values corresponding to one feature in the MDR model
    labels: array-like (# samples)
        The class labels corresponding to features X and Y
    base: integer (default: 2)
        The base in which to calculate MDR mutual information

    Returns
    ----------
    mdr_mutual_information: float
        The MDR mutual information calculated according to the equation I(XY;labels) = H(labels) - H(labels|XY)

    """
    return mutual_information(_mdr_predict(X, Y, labels), labels, base=base)

def n_way_models(mdr_instance, X, y, n=[2], feature_names=None):
    """Fits a MDR model to all n-way combinations of the features in X.

    Note that this function performs an exhaustive search through all feature combinations and can be computationally expensive.

    Parameters
    ----------
    mdr_instance: object
        An instance of the MDR type to use.
    X: array-like (# rows, # features)
        NumPy matrix containing the features
    y: array-like (# rows, 1)
        NumPy matrix containing the target values
    n: list (default: [2])
        The maximum size(s) of the MDR model to generate.
        e.g., if n == [3], all 3-way models will be generated.
    feature_names: list (default: None)
        The corresponding names of the features in X.
        If None, then the features will be named according to their order.

    Returns
    ----------
    (fitted_model, fitted_model_score, fitted_model_features): tuple of (list, list, list)
        fitted_model contains the MDR model fitted to the data.
        fitted_model_score contains the training scores corresponding to the fitted MDR model.
        fitted_model_features contains a list of the names of the features that were used in the corresponding model.

    """
    if feature_names is None:
        feature_names = list(range(X.shape[1]))

    for cur_n in n:
        for features in itertools.combinations(range(X.shape[1]), cur_n):
            mdr_model = copy.deepcopy(mdr_instance)
            mdr_model.fit(X[:, features], y)
            mdr_model_score = mdr_model.score(X[:, features], y)
            model_features = [feature_names[feature] for feature in features]
            yield mdr_model, mdr_model_score, model_features

def plot_mdr_grid(mdr_instance):
    """Visualizes the MDR grid of a given fitted MDR instance. Only works for 2-way MDR models.

    This function is currently incomplete.

    Parameters
    ----------
    mdr_instance: object
        A fitted instance of the MDR type to visualize.

    Returns
    ----------
    fig: matplotlib.figure
        Figure object for the visualized MDR grid.

    """
    var1_levels = list(set([variables[0] for variables in mdr_instance.feature_map]))
    var2_levels = list(set([variables[1] for variables in mdr_instance.feature_map]))
    max_count = np.array(list(mdr_instance.class_count_matrix.values())).flatten().max()

    """
    TODO:
        - Add common axis labels
        - Make sure this scales for smaller and larger record sizes
        - Extend to 3-way+ models, e.g., http://4.bp.blogspot.com/-vgKCjEkWFUc/UPwPuHo6XvI/AAAAAAAAAE0/fORHqDcoikE/s1600/model.jpg
    """

    fig, splots = plt.subplots(ncols=len(var1_levels), nrows=len(var2_levels), sharey=True, sharex=True)
    fig.set_figwidth(6)
    fig.set_figheight(6)

    for (var1, var2) in itertools.product(var1_levels, var2_levels):
        class_counts = mdr_instance.class_count_matrix[(var1, var2)]
        splot = splots[var2_levels.index(var2)][var1_levels.index(var1)]
        splot.set_yticks([])
        splot.set_xticks([])
        splot.set_ylim(0, max_count * 1.5)
        splot.set_xlim(-0.5, 1.5)

        if var2_levels.index(var2) == 0:
            splot.set_title('X1 = {}'.format(var1), fontsize=12)
        if var1_levels.index(var1) == 0:
            splot.set_ylabel('X2 = {}'.format(var2), fontsize=12)

        bars = splot.bar(left=range(class_counts.shape[0]),
                         height=class_counts, width=0.5,
                         color='black', align='center')

        bgcolor = 'lightgrey' if mdr_instance.feature_map[(var1, var2)] == 0 else 'darkgrey'
        splot.set_axis_bgcolor(bgcolor)
        for index, bar in enumerate(bars):
            splot.text(index, class_counts[index] + (max_count * 0.1), class_counts[index], ha='center')

    fig.tight_layout()
    return fig


"""
=============
CLI工具包
=============
"""

import fileinput
import importlib
import os
import shutil
import sys

import click
from flask import current_app as app
from flask.cli import with_appcontext

@click.group()
def ff():
    """
    Fantasy tool-box
    """
    pass

@ff.command()
@click.option('--migrations-root', type=click.Path(exists=False))
@with_appcontext
def makemigrations(migrations_root):
    """等价于 django makemigrations 操作"""
    from flask_migrate import (Migrate, init as migrate_init,
                               migrate as migrate_exec)

    migrations_root = migrations_root or os.path.join(
        os.environ.get('FANTASY_MIGRATION_PATH',
                       os.getcwd()),
        'migrations')

    migrations_root = os.path.expanduser(migrations_root)

    mig = Migrate(app, app.db, directory=migrations_root)

    if not os.path.exists(migrations_root):
        migrate_init(migrations_root)
        pass

    models_file = os.path.join(migrations_root, 'models.txt')

    if not os.path.exists(models_file):
        with open(models_file, 'w') as fw:
            fw.write('# add module name in this file.')
            pass
        pass

    with open(models_file, 'r') as fp:
        modules = fp.readlines()
        pass

    modules = filter(lambda x: x.strip("\n"), modules)
    modules = map(lambda x: x.strip("\n").split("#")[0].strip(), modules)
    modules = list(filter(lambda x: x, modules))

    if not modules:
        click.echo(
            click.style('No models found,'
                        'skip create migrations...'
                        'You need edit models.txt file set your module',
                        fg='yellow'))
        sys.exit(0)

    for m in modules:
        importlib.import_module(m + '.models')
        pass

    migrate_exec(migrations_root)
    mig.init_app(app, app.db)
    pass

@ff.command()
@click.option('--migrations-root', type=click.Path(exists=False))
@with_appcontext
def migrate(migrations_root):
    """等价于 django migrate 操作"""
    from flask_migrate import Migrate, upgrade as migrate_upgrade
    from flask_sqlalchemy import SQLAlchemy
    from sqlalchemy.engine.url import make_url
    from sqlalchemy_utils import database_exists, create_database

    db = SQLAlchemy()
    dsn = make_url(app.config['SQLALCHEMY_DATABASE_URI'])
    if not database_exists(dsn):
        create_database(dsn)
        pass

    migrations_root = migrations_root or os.path.join(
        os.environ.get('FANTASY_MIGRATION_PATH',
                       os.getcwd()),
        'migrations')
    migrations_root = os.path.expanduser(migrations_root)

    if os.path.exists(migrations_root):
        mig = Migrate(app, db, directory=migrations_root)
        mig.init_app(app, db)
        migrate_upgrade(migrations_root)
    else:
        click.echo(
            click.style('migration files not exist,skip migrate...', fg='red'))
        sys.exit(-1)
    pass

@ff.command()
@click.option('--work-dir', type=click.Path(exists=False))
@click.option('--hive-root', type=click.Path(exists=True),
              default=os.path.expanduser('~/Codes/1024/hive/'))
@click.option('--with-requirements/--without-requirements', default=False)
@click.option('--with-dockerfile/--without-dockerfile', default=False)
@click.option('--active-module', '-a',
              multiple=True, help='load order will effect generate file')
@click.option('--active-module-file', '-A',
              type=click.Path(exists=True))
@with_appcontext
def requirements(work_dir, hive_root, with_requirements,
                 with_dockerfile, active_module, active_module_file):
    """编译全新依赖文件"""
    import sys
    sys.path.insert(0, hive_root)

    hive_root = os.path.abspath(os.path.expanduser(hive_root))

    work_dir = work_dir or os.path.join(
        os.environ.get('FANTASY_APP_PATH',
                       os.getcwd()))

    work_dir = os.path.expanduser(work_dir)

    requirements_root = os.path.join(work_dir, 'requirements')
    migrate_root = os.path.join(work_dir, 'migrations')

    # active_modules 严格按照顺序
    active_module_paths = []
    active_module_list = []

    if active_module_file:
        with open(active_module_file, 'r') as fp:
            for l in fp:
                pkg = l.split('#')[0].strip()
                if pkg:
                    active_module_list.append(l.strip("\n"))
            pass

    active_module_list += active_module

    for m in active_module_list:
        try:
            mod = importlib.import_module(m)
            active_module_paths.append(os.path.dirname(mod.__file__))
        except ImportError:
            click.echo('module "%s" not found.' % m, color="yellow")
            pass
        pass

    def build_requirements():
        """构造requirements文件

        requirements文件共分为两份：

        - hive.txt  从hive项目中直接复制
        - hive-modules.txt 从指定的模块中装载依赖项

        .. note::
            requirements要求必须是顺序无关的
            因为我们会使用set来去重，并按照value排序

        """
        if not os.path.exists(requirements_root):
            os.makedirs(requirements_root)
            pass

        click.echo(click.style("Generate hive requirements...", fg="yellow"))

        shutil.copy(
            os.path.join(hive_root, 'requirements.txt'),
            os.path.join(requirements_root, 'hive.txt')
        )

        click.echo(click.style("Generate hive-module requirements...",
                               fg="yellow"))

        requirements_files = []

        for m in active_module_paths:
            t = os.path.join(m, 'requirements.txt')
            if os.path.exists(t):
                requirements_files.append(t)
            pass

        module_packages = set()
        with fileinput.input(requirements_files) as fp:
            for line in fp:
                pkg = line.split('#')[0].strip()
                if pkg:
                    module_packages.add(pkg)
            pass

        with click.open_file(
                os.path.join(requirements_root, 'hive-modules.txt'),
                'w') as fp:
            for p in module_packages:
                fp.write("%s\n" % p)
            pass
        pass

    def build_dockerfile():
        """构造Dockerfile"""

        modules_in_hive = map(
            lambda x: x.replace(hive_root, '').lstrip('/'),
            filter(lambda x: x.startswith(hive_root),
                   active_module_paths))

        modules_path = ' '.join(modules_in_hive)

        docker_file = os.path.join(
            os.path.dirname(requirements_root),
            'Dockerfile'
        )

        # update Dockerfile
        if os.path.exists(docker_file):
            click.echo(click.style("Found Dockerfile,try update...",
                                   fg="yellow"))

            with open(docker_file, 'r') as fp:
                buffer = fp.read()
                pass
            import re
            replaced = re.sub('ARG HIVE_PACKAGES=".*"',
                              'ARG HIVE_PACKAGES="%s"' % modules_path,
                              buffer)
            with open(docker_file, 'w') as fp:
                fp.write(replaced)
                pass

            pass
        pass

    def build_migrations():
        models_pairs = filter(
            lambda pair: os.path.exists(pair[0]),
            map(lambda x: (os.path.join(x[0], 'models.py'), x[1]),
                [(v, active_module_list[i]) for i, v in
                 enumerate(active_module_paths)]))

        try:
            _, models = zip(*models_pairs)
        except ValueError:
            click.echo(click.style("No models found,"
                                   "is it include in "
                                   "your PYTHONPATH?\n"
                                   "Modules: %s" %
                                   ','.join(active_module_list),
                                   fg="yellow"))
            return

        click.echo(click.style("Found models.txt,try update...",
                               fg="yellow"))

        with open(os.path.join(migrate_root, 'models.txt'), 'w') as fp:
            for p in models:
                fp.write("%s\n" % p)
            pass
        pass

    def build_tasks():
        tasks_pairs = filter(
            lambda pair: os.path.exists(pair[0]),
            map(lambda x: (os.path.join(x[0], 'tasks.py'), x[1]),
                [(v, active_module_list[i]) for i, v in
                 enumerate(active_module_paths)]))

        try:
            _, tasks = zip(*tasks_pairs)
        except ValueError:
            click.echo(click.style("No tasks found,"
                                   "is it include in "
                                   "your PYTHONPATH?\n"
                                   "Modules: %s" %
                                   ','.join(active_module_list),
                                   fg="yellow"))
            return

        click.echo(click.style("Found tasks.txt,try update...",
                               fg="yellow"))
        with open(os.path.join(migrate_root, 'tasks.txt'), 'w') as fp:
            for p in tasks:
                fp.write("%s\n" % p)
        pass

    if with_requirements:
        build_requirements()

    if with_dockerfile:
        build_dockerfile()

    if os.path.exists(migrate_root):
        build_migrations()

    if os.path.exists(migrate_root):
        build_tasks()

    click.echo(click.style("Generate done...", fg="yellow"))
    pass

@ff.command()
@click.option('--celery-arguments', '-a', type=click.STRING,
              help='celery worker command arguments')
@with_appcontext
def queue(celery_arguments):
    """启动队列服务[开发中]"""

    if not app.celery:
        return click.echo(
            click.style('No celery config found，skip start...', fg='yellow'))

    celery = app.celery
    celery.autodiscover_tasks()

    argv = celery_arguments.split()
    argv.insert(0, 'worker')
    argv.insert(0, 'Queue')
    celery.worker_main(argv)
    pass


"""
=============
Fantasy
=============

Fantasy在我所做的基于Flask项目中一直发挥着作用。
虽然还有很大的优化空间，但从目前看，效果也算是稳定，符合预期。

在我的项目中，Fantasy是为了解决在团队内部开发的Hive-App（本质上是Flask App）之间
构造一层。提供通用的工具链，模块之间的数据尽可能的复用。

目前因为入职的关系，考虑到版权相关的风险因素，所以将这部分代码开源。
后期也会持续提供迭代改进。

"""

import importlib
import os

from flask import Flask

version = "0.2.15"
version_info = version.split('.')

os.environ.setdefault('FANTASY_ACTIVE_DB', 'yes')
os.environ.setdefault('FANTASY_ACTIVE_CACHE', 'yes')
os.environ.setdefault('FANTASY_ACTIVE_SENTRY', 'no')
os.environ.setdefault('FANTASY_ACTIVE_ACCOUNT', 'yes')
os.environ.setdefault('FANTASY_ACCOUNT_SECURITY_MODE', 'no')
os.environ.setdefault('FANTASY_ACCOUNT_MODEL', 'account.models.Account')
os.environ.setdefault('FANTASY_ROLE_MODEL', 'account.models.Role')
os.environ.setdefault('FANTASY_PRIMARY_NODE', 'no')
os.environ.setdefault('FANTASY_TRACK_MODE', 'no')

_db = None
if os.environ.get('FANTASY_ACTIVE_DB', 'no') != 'no':
    from flask_sqlalchemy import SQLAlchemy

    _db = SQLAlchemy()
    pass

class FantasyFlask(Flask):
    db = None
    cache = None
    celery = None
    pass

def smart_database(app):
    """尝试对数据库做初始化操作"""

    from sqlalchemy.engine.url import make_url
    from sqlalchemy_utils import database_exists, create_database

    # 如果数据库不存在，则尝试创建数据
    dsn = make_url(app.config['SQLALCHEMY_DATABASE_URI'])
    if not database_exists(dsn):
        create_database(dsn)
        pass
    pass

def smart_migrate(app, migrations_root):
    """如果存在migration且指定为primary_node则执行migrate操作"""

    db = app.db
    if os.path.exists(migrations_root) and \
            os.environ['FANTASY_PRIMARY_NODE'] != 'no':
        from flask_migrate import (Migrate,
                                   upgrade as migrate_upgrade)

        migrate = Migrate(app, db, directory=migrations_root)
        migrate.init_app(app, db)
        migrate_upgrade(migrations_root)
        pass
    pass

def smart_account(app):
    """尝试使用内置方式构建账户"""
    if os.environ['FANTASY_ACTIVE_ACCOUNT'] == 'no':
        return

    from flask_security import SQLAlchemyUserDatastore, Security

    account_module_name, account_class_name = os.environ[
        'FANTASY_ACCOUNT_MODEL'].rsplit('.', 1)

    account_module = importlib.import_module(account_module_name)
    account_class = getattr(account_module, account_class_name)

    role_module_name, role_class_name = os.environ[
        'FANTASY_ROLE_MODEL'].rsplit('.', 1)
    role_module = importlib.import_module(role_module_name)
    role_class = getattr(role_module, role_class_name)

    r = True if os.environ[
                    'FANTASY_ACCOUNT_SECURITY_MODE'] != 'no' else False

    Security(app,
             SQLAlchemyUserDatastore(
                 app.db, account_class, role_class),
             register_blueprint=r)
    pass

def smart_logging():
    """
    ..deprecated::

        弃用，应该通过类似run_app注入的方式来完成

    :return:
    """
    import sys
    from logging.config import dictConfig

    dictConfig({
        'version': 1,
        'formatters': {'default': {
            'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
        }},
        'handlers': {'console': {
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'formatter': 'default'
        }},
        'root': {
            'level': 'INFO',
            'handlers': ['console']
        },
        'shopping': {
            'level': 'DEBUG',
            'handlers': ['console']
        }
    })
    pass

def load_tasks(app, entry_file=None):
    """装载任务，解决celery无法自动装载的问题"""
    from celery import Task
    tasks_txt = os.path.join(os.path.dirname(entry_file), 'migrations',
                             'tasks.txt')

    if not os.path.exists(tasks_txt):
        import sys
        print('Tasks file not found:%s' % tasks_txt)
        sys.exit(-1)

    class ContextTask(Task):
        abstract = True

        def __call__(self, *args, **kwargs):
            with app.app_context():
                return super().__call__(*args, **kwargs)

    app.celery.config_from_object(app.config, namespace='CELERY')
    app.celery.Task = ContextTask

    with app.app_context():
        with open(tasks_txt, 'r') as f:
            for line in f:
                mod = line.strip('\n')
                if mod:
                    importlib.import_module(mod + '.tasks')
                pass
            pass
        pass
    pass

def create_celery(name):
    from .celery import Celery

    celery = Celery(name,
                    backend=os.environ['CELERY_RESULT_BACKEND'],
                    broker=os.environ['CELERY_BROKER_URL'])
    return celery

def create_app(app_name, config={}, db=None, celery=None):
    """
    App Factory 工具

    策略是：
        - 初始化app
        - 根据app_name,装载指定的模块
        - 尝试装载app.run_app
        - 如果指定了`FANTASY_PRIMARY_NODE`，则尝试进行migrate操作
        - 装载error handler

    :return:
    """

    track_mode = os.environ['FANTASY_TRACK_MODE'] == 'yes'
    if track_mode:
        print('(00/14)fantasy track mode active...')

    active_db = os.environ['FANTASY_ACTIVE_DB'] == 'yes'

    if track_mode:
        print('(01/14)hacking webargs...')

    from webargs.flaskparser import parser
    from . import error_handler, hacker, cli

    hacker.hack_webargs()

    migrations_root = os.path.join(
        os.environ.get('FANTASY_MIGRATION_PATH',
                       os.getcwd()),
        'migrations')

    if track_mode:
        print('(02/14)initial app...')

    mod = importlib.import_module(app_name)
    app = FantasyFlask(__name__, root_path=os.path.dirname(mod.__file__))

    if track_mode:
        print('(03/14)update app.config...')

    if config:
        app.config.update(config)

    # 由外部做显式声明，否则不做调用
    config_module = os.environ.get('FANTASY_SETTINGS_MODULE', None)
    if track_mode:
        print("       found config module %s,try load it..." % config_module)

    if config_module:
        app.config.from_object(config_module)

    if track_mode:
        print('(04/14)confirm celery...')

    if celery:
        app.celery = celery
        pass

    if track_mode:
        print('(05/14)bind app context...')

    with app.app_context():

        if track_mode:
            print('(06/14)confirm db handle...')

        if db is None:
            global _db
            app.db = _db
        else:
            app.db = db

        if track_mode:
            print('(07/14)confirm cache...')

        if os.environ['FANTASY_ACTIVE_CACHE'] != 'no':
            from flask_caching import Cache
            app.cache = Cache(app, config=app.config)
            pass

        if track_mode:
            print('(08/14)confirm sentry...')

        if os.environ.get('FANTASY_ACTIVE_SENTRY') != 'no':
            from raven.contrib.flask import Sentry
            Sentry(app)
            pass

        if track_mode:
            print('(09/14)active app...')

        if hasattr(mod, 'run_app'):
            run_app = getattr(mod, 'run_app')

            try:
                run_app(app)
            except Exception as e:
                if hasattr(app, 'sentry'):
                    app.sentry.handle_exception(e)
                    pass

                import sys
                import traceback
                traceback.print_exc()
                sys.exit(-1)

            pass

        if active_db and app.db:
            if track_mode:
                print('(10/14)trigger auto migrate...')

            smart_database(app)
            smart_migrate(app, migrations_root)
            smart_account(app)
            app.db.init_app(app)

            @app.teardown_request
            def session_clear(exception=None):
                if exception and app.db.session.is_active:
                    app.db.session.rollback()
                    app.db.session.remove()

            pass

        if track_mode:
            print('(11/14)bind error handle...')

        # 添加错误控制
        @parser.error_handler
        def h_webargs(error):
            return error_handler.webargs_error(error)

        @app.errorhandler(422)
        def h_422(error):
            return error_handler.http422(error)

        @app.errorhandler(500)
        def h_500(error):
            return error_handler.http500(error)

        if hasattr(mod, 'error_handler'):
            error_handle = getattr(mod, 'error_handle')
            error_handle(app)
            pass

        if track_mode:
            print('(12/14)bind admin handle...')

        if hasattr(mod, 'run_admin'):
            import flask_admin
            admin = flask_admin.Admin(name=os.environ.get('FANTASY_ADMIN_NAME',
                                                          'Admin'),
                                      template_mode=os.environ.get(
                                          'FANTASY_ADMIN_TEMPLATE_MODE',
                                          'bootstrap3'))

            run_admin = getattr(mod, 'run_admin')
            run_admin(admin)
            admin.init_app(app)
            pass

        pass

    if track_mode:
        print('(13/14)bind ff command...')

    app.cli.add_command(cli.ff)

    if track_mode:
        print('(14/14)bind cli command...')

    if hasattr(mod, 'run_cli'):
        run_cli = getattr(mod, 'run_cli')
        run_cli(app)
        pass

    return app


"""
=============
全局通用库函数
=============
"""

from flask import current_app
from werkzeug.local import LocalProxy

_fantasy = LocalProxy(lambda: current_app.extensions['security'])
localize_callback = LocalProxy(lambda: _fantasy.i18n_domain.gettext)

def get_config(app, prefix='hive_'):
    """Conveniently get the security configuration for the specified
    application without the annoying 'SECURITY_' prefix.

    :param app: The application to inspect
    """
    items = app.config.items()
    prefix = prefix.upper()

    def strip_prefix(tup):
        return (tup[0].replace(prefix, ''), tup[1])

    return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)])

def config_value(key, app=None, default=None, prefix='hive_'):
    """Get a Flask-Security configuration value.

    :param key: The configuration key without the prefix `SECURITY_`
    :param app: An optional specific application to inspect. Defaults to
                Flask's `current_app`
    :param default: An optional default value if the value is not set
    """
    app = app or current_app
    return get_config(app, prefix=prefix).get(key.upper(), default)


"""
=======================
随机字符串生成的补充函数
=======================
"""

import random
import string

def random_str(length=16, only_digits=False):
    """
    生成随机字符串
    :return:
    """

    choices = string.digits
    if not only_digits:
        choices += string.ascii_uppercase

    return ''.join(random.SystemRandom().choice(choices)
                   for _ in range(length))

def random_number(length=6):
    return random_str(length, only_digits=True)


from typing import Iterable, Optional, TypeVar

from pyrsistent import PVector, pvector  # noqa # pylint: disable=unused-import

from basilisp.lang.interfaces import (
    ILispObject,
    IMeta,
    IPersistentMap,
    IPersistentVector,
    ISeq,
)
from basilisp.lang.obj import seq_lrepr as _seq_lrepr
from basilisp.lang.seq import sequence

T = TypeVar("T")

class Vector(ILispObject, IMeta, IPersistentVector[T]):  # type: ignore
    """Basilisp Vector. Delegates internally to a pyrsistent.PVector object.
    Do not instantiate directly. Instead use the v() and vec() factory
    methods below."""

    __slots__ = ("_inner", "_meta")

    def __init__(self, wrapped: "PVector[T]", meta=None) -> None:
        self._inner = wrapped
        self._meta = meta

    def __contains__(self, item):
        return item in self._inner

    def __eq__(self, other):
        if hasattr(other, "__len__"):
            return self._inner == other
        return all(e1 == e2 for e1, e2 in zip(self._inner, other))

    def __getitem__(self, item):
        if isinstance(item, slice):
            return Vector(self._inner[item])
        return self._inner[item]

    def __hash__(self):
        return hash(self._inner)

    def __iter__(self):
        yield from self._inner

    def __len__(self):
        return len(self._inner)

    def _lrepr(self, **kwargs) -> str:
        return _seq_lrepr(self._inner, "[", "]", meta=self._meta, **kwargs)

    @property
    def meta(self):
        return self._meta

    def with_meta(self, meta) -> "Vector[T]":
        new_meta = meta if self._meta is None else self._meta.update(meta)
        return vector(self._inner, meta=new_meta)

    def cons(self, *elems: T) -> "Vector[T]":  # type: ignore
        e = self._inner.evolver()
        for elem in elems:
            e.append(elem)
        return Vector(e.persistent(), meta=self.meta)

    def assoc(self, *kvs: T) -> "Vector[T]":
        return Vector(self._inner.mset(*kvs))  # type: ignore

    def contains(self, k):
        return 0 <= k < len(self._inner)

    def entry(self, k, default=None):
        try:
            return self._inner[k]
        except IndexError:
            return default

    @staticmethod
    def empty() -> "Vector[T]":
        return v()

    def seq(self) -> ISeq[T]:  # type: ignore
        return sequence(self)

    def peek(self) -> Optional[T]:
        if len(self) == 0:
            return None
        return self[-1]

    def pop(self) -> "Vector[T]":
        if len(self) == 0:
            raise IndexError("Cannot pop an empty vector")
        return self[:-1]

def vector(members: Iterable[T], meta: Optional[IPersistentMap] = None) -> Vector[T]:
    """Creates a new vector."""
    return Vector(pvector(members), meta=meta)

def v(*members: T, meta: Optional[IPersistentMap] = None) -> Vector[T]:
    """Creates a new vector from members."""
    return Vector(pvector(members), meta=meta)


import atexit
import importlib
import os.path
import platform
import traceback
import types
from typing import Any

import click
import pytest

import basilisp.lang.compiler as compiler
import basilisp.lang.reader as reader
import basilisp.lang.runtime as runtime
import basilisp.lang.symbol as sym
import basilisp.main as basilisp

CLI_INPUT_FILE_PATH = "<CLI Input>"
BASILISP_REPL_HISTORY_FILE_PATH = os.path.join(
    os.path.expanduser("~"), ".basilisp_history"
)
BASILISP_REPL_HISTORY_LENGTH = 1000
REPL_INPUT_FILE_PATH = "<REPL Input>"
STDIN_INPUT_FILE_PATH = "<stdin>"
STDIN_FILE_NAME = "-"

try:
    import readline
except ImportError:  # pragma: no cover
    pass
else:
    readline.parse_and_bind("tab: complete")
    readline.set_completer_delims("()[]{} \n\t")
    readline.set_completer(runtime.repl_complete)

    try:
        readline.read_history_file(BASILISP_REPL_HISTORY_FILE_PATH)
        readline.set_history_length(BASILISP_REPL_HISTORY_LENGTH)
    except FileNotFoundError:  # pragma: no cover
        pass
    except Exception:  # noqa  # pragma: no cover
        # PyPy 3.6's ncurses implementation throws an error here
        if platform.python_implementation() != "PyPy":
            raise
    else:
        atexit.register(readline.write_history_file, BASILISP_REPL_HISTORY_FILE_PATH)

@click.group()
def cli():
    """Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."""

def eval_file(filename: str, ctx: compiler.CompilerContext, module: types.ModuleType):
    """Evaluate a file with the given name into a Python module AST node."""
    last = None
    for form in reader.read_file(filename, resolver=runtime.resolve_alias):
        last = compiler.compile_and_exec_form(form, ctx, module)
    return last

def eval_stream(stream, ctx: compiler.CompilerContext, module: types.ModuleType):
    """Evaluate the forms in stdin into a Python module AST node."""
    last = None
    for form in reader.read(stream, resolver=runtime.resolve_alias):
        last = compiler.compile_and_exec_form(form, ctx, module)
    return last

def eval_str(s: str, ctx: compiler.CompilerContext, module: types.ModuleType, eof: Any):
    """Evaluate the forms in a string into a Python module AST node."""
    last = eof
    for form in reader.read_str(s, resolver=runtime.resolve_alias, eof=eof):
        last = compiler.compile_and_exec_form(form, ctx, module)
    return last

def bootstrap_repl(which_ns: str) -> types.ModuleType:
    """Bootstrap the REPL with a few useful vars and returned the
    bootstrapped module so it's functions can be used by the REPL
    command."""
    repl_ns = runtime.Namespace.get_or_create(sym.symbol("basilisp.repl"))
    ns = runtime.Namespace.get_or_create(sym.symbol(which_ns))
    repl_module = importlib.import_module("basilisp.repl")
    ns.add_alias(sym.symbol("basilisp.repl"), repl_ns)
    ns.refer_all(repl_ns)
    return repl_module

@cli.command(short_help="start the Basilisp REPL")
@click.option(
    "--default-ns",
    default=runtime.REPL_DEFAULT_NS,
    help="default namespace to use for the REPL",
)
@click.option(
    "--use-var-indirection",
    default=False,
    is_flag=True,
    envvar="BASILISP_USE_VAR_INDIRECTION",
    help="if provided, all Var accesses will be performed via Var indirection",
)
@click.option(
    "--warn-on-shadowed-name",
    default=False,
    is_flag=True,
    envvar="BASILISP_WARN_ON_SHADOWED_NAME",
    help="if provided, emit warnings if a local name is shadowed by another local name",
)
@click.option(
    "--warn-on-shadowed-var",
    default=False,
    is_flag=True,
    envvar="BASILISP_WARN_ON_SHADOWED_VAR",
    help="if provided, emit warnings if a Var name is shadowed by a local name",
)
@click.option(
    "--warn-on-var-indirection",
    default=True,
    is_flag=True,
    envvar="BASILISP_WARN_ON_VAR_INDIRECTION",
    help="if provided, emit warnings if a Var reference cannot be direct linked",
)
def repl(
    default_ns,
    use_var_indirection,
    warn_on_shadowed_name,
    warn_on_shadowed_var,
    warn_on_var_indirection,
):
    basilisp.init()
    repl_module = bootstrap_repl(default_ns)
    ctx = compiler.CompilerContext(
        filename=REPL_INPUT_FILE_PATH,
        opts={
            compiler.WARN_ON_SHADOWED_NAME: warn_on_shadowed_name,
            compiler.WARN_ON_SHADOWED_VAR: warn_on_shadowed_var,
            compiler.USE_VAR_INDIRECTION: use_var_indirection,
            compiler.WARN_ON_VAR_INDIRECTION: warn_on_var_indirection,
        },
    )
    ns_var = runtime.set_current_ns(default_ns)
    eof = object()
    while True:
        ns: runtime.Namespace = ns_var.value
        try:
            lsrc = input(f"{ns.name}=> ")
        except EOFError:
            break
        except KeyboardInterrupt:  # pragma: no cover
            print("")
            continue

        if len(lsrc) == 0:
            continue

        try:
            result = eval_str(lsrc, ctx, ns.module, eof)
            if result is eof:  # pragma: no cover
                continue
            print(runtime.lrepr(result))
            repl_module.mark_repl_result(result)  # type: ignore
        except reader.SyntaxError as e:
            traceback.print_exception(reader.SyntaxError, e, e.__traceback__)
            repl_module.mark_exception(e)  # type: ignore
            continue
        except compiler.CompilerException as e:
            traceback.print_exception(compiler.CompilerException, e, e.__traceback__)
            repl_module.mark_exception(e)  # type: ignore
            continue
        except Exception as e:
            traceback.print_exception(Exception, e, e.__traceback__)
            repl_module.mark_exception(e)  # type: ignore
            continue

@cli.command(short_help="run a Basilisp script or code")
@click.argument("file-or-code")
@click.option(
    "-c", "--code", is_flag=True, help="if provided, treat argument as a string of code"
)
@click.option(
    "--in-ns", default=runtime.REPL_DEFAULT_NS, help="namespace to use for the code"
)
@click.option(
    "--use-var-indirection",
    default=False,
    is_flag=True,
    envvar="BASILISP_USE_VAR_INDIRECTION",
    help="if provided, all Var accesses will be performed via Var indirection",
)
@click.option(
    "--warn-on-shadowed-name",
    default=False,
    is_flag=True,
    envvar="BASILISP_WARN_ON_SHADOWED_NAME",
    help="if provided, emit warnings if a local name is shadowed by another local name",
)
@click.option(
    "--warn-on-shadowed-var",
    default=False,
    is_flag=True,
    envvar="BASILISP_WARN_ON_SHADOWED_VAR",
    help="if provided, emit warnings if a Var name is shadowed by a local name",
)
@click.option(
    "--warn-on-var-indirection",
    default=True,
    is_flag=True,
    envvar="BASILISP_WARN_ON_VAR_INDIRECTION",
    help="if provided, emit warnings if a Var reference cannot be direct linked",
)
def run(  # pylint: disable=too-many-arguments
    file_or_code,
    code,
    in_ns,
    use_var_indirection,
    warn_on_shadowed_name,
    warn_on_shadowed_var,
    warn_on_var_indirection,
):
    """Run a Basilisp script or a line of code, if it is provided."""
    basilisp.init()
    ctx = compiler.CompilerContext(
        filename=CLI_INPUT_FILE_PATH
        if code
        else (
            STDIN_INPUT_FILE_PATH if file_or_code == STDIN_FILE_NAME else file_or_code
        ),
        opts={
            compiler.WARN_ON_SHADOWED_NAME: warn_on_shadowed_name,
            compiler.WARN_ON_SHADOWED_VAR: warn_on_shadowed_var,
            compiler.USE_VAR_INDIRECTION: use_var_indirection,
            compiler.WARN_ON_VAR_INDIRECTION: warn_on_var_indirection,
        },
    )
    eof = object()

    with runtime.ns_bindings(in_ns) as ns:
        if code:
            print(runtime.lrepr(eval_str(file_or_code, ctx, ns.module, eof)))
        elif file_or_code == STDIN_FILE_NAME:
            print(
                runtime.lrepr(
                    eval_stream(click.get_text_stream("stdin"), ctx, ns.module)
                )
            )
        else:
            print(runtime.lrepr(eval_file(file_or_code, ctx, ns.module)))

@cli.command(short_help="run tests in a Basilisp project")
@click.argument("args", nargs=-1)
def test(args):  # pragma: no cover
    """Run tests in a Basilisp project."""
    pytest.main(args=list(args))

@cli.command(short_help="print the version of Basilisp")
def version():
    from basilisp.__version__ import __version__

    print(f"Basilisp {__version__}")

if __name__ == "__main__":
    cli()


from typing import Any, Callable, Generic, Optional, TypeVar

import basilisp.lang.atom as atom
import basilisp.lang.map as lmap
import basilisp.lang.symbol as sym
from basilisp.util import Maybe

T = TypeVar("T")
DispatchFunction = Callable[..., T]
Method = Callable[..., Any]

class MultiFunction(Generic[T]):
    __slots__ = ("_name", "_default", "_dispatch", "_methods")

    def __init__(
        self, name: sym.Symbol, dispatch: DispatchFunction, default: T
    ) -> None:
        self._name = name  # pylint:disable=assigning-non-slot
        self._default = default  # pylint:disable=assigning-non-slot
        self._dispatch = dispatch  # pylint:disable=assigning-non-slot
        self._methods: atom.Atom = atom.Atom(  # pylint:disable=assigning-non-slot
            lmap.Map.empty()
        )

    def __call__(self, *args, **kwargs):
        key = self._dispatch(*args, **kwargs)
        method_cache = self.methods
        method: Optional[Method] = Maybe(method_cache.entry(key, None)).or_else(
            lambda: method_cache.entry(self._default, None)  # type: ignore
        )
        if method:
            return method(*args, **kwargs)
        raise NotImplementedError

    @staticmethod
    def __add_method(m: lmap.Map, key: T, method: Method) -> lmap.Map:
        """Swap the methods atom to include method with key."""
        return m.assoc(key, method)

    def add_method(self, key: T, method: Method) -> None:
        """Add a new method to this function which will respond for
        key returned from the dispatch function."""
        self._methods.swap(MultiFunction.__add_method, key, method)

    def get_method(self, key: T) -> Optional[Method]:
        """Return the method which would handle this dispatch key or
        None if no method defined for this key and no default."""
        method_cache = self.methods
        # The 'type: ignore' comment below silences a spurious MyPy error
        # about having a return statement in a method which does not return.
        return Maybe(method_cache.entry(key, None)).or_else(
            lambda: method_cache.entry(self._default, None)  # type: ignore
        )

    @staticmethod
    def __remove_method(m: lmap.Map, key: T) -> lmap.Map:
        """Swap the methods atom to remove method with key."""
        return m.dissoc(key)

    def remove_method(self, key: T) -> Optional[Method]:
        """Remove the method defined for this key and return it."""
        method = self.methods.entry(key, None)
        if method:
            self._methods.swap(MultiFunction.__remove_method, key)
        return method

    def remove_all_methods(self) -> None:
        """Remove all methods defined for this multi-function."""
        self._methods.reset(lmap.Map.empty())

    @property
    def default(self) -> T:
        return self._default

    @property
    def methods(self) -> lmap.Map:
        return self._methods.deref()

def multifn(dispatch: DispatchFunction, default=None) -> MultiFunction[T]:
    """Decorator function which can be used to make Python multi functions."""
    name = sym.symbol(dispatch.__qualname__, ns=dispatch.__module__)
    return MultiFunction(name, dispatch, default)