#!/usr/bin/python
# ----------------------------------------------------------------------------
# cocos-console: command line tool manager for cocos2d-x
#
# Author: Ricardo Quesada
# Copyright 2013 (C) Zynga, Inc
#
# License: MIT
# ----------------------------------------------------------------------------
'''
Command line tool manager for cocos
'''

__docformat__ = 'restructuredtext'


# python
import sys
import os
import subprocess
from contextlib import contextmanager
import axmol_project
import shutil
import string
import locale
import gettext
import json
import utils
import re


# FIXME: MultiLanguage should be deprecated in favor of gettext
from MultiLanguage import MultiLanguage

AXOML_CONSOLE_VERSION = '1.0'

def dict_contains(dict,key):
    if(sys.version_info.major >= 3):
        return (key in dict)
    else:
        return dict.has_key(key)

def encode_with(text,encoding):
    if(sys.version_info.major >= 3):
        return text
    else:
        return unicode(text, encoding)

def str_join(s, v):
    if(sys.version_info.major >= 3):
        return s.join(v)
    else:
        return string.join(v, s)

def isunicode(text):
    if(sys.version_info.major >= 3):
        return isinstance(text, str)
    else:
        return isinstance(text, unicode)

def transcode(text,encoding):
    if(sys.version_info.major >= 3 and encoding=="utf-8"):
        return text
    else:
        return text.encode(encoding)

def get_input(prompt):
    if(sys.version_info.major >= 3):
        return input(prompt)
    else:
        return raw_input(prompt)

class Cocos2dIniParser:
    def __init__(self):
        if(sys.version_info.major >= 3):
            import configparser # import ConfigParser
            self._cp = configparser.ConfigParser(allow_no_value=True)
        else:
            import ConfigParser
            self._cp = ConfigParser.ConfigParser(allow_no_value=True)

        self._cp.optionxform = str

        # read global config file
        self.cocos2d_path = os.path.dirname(os.path.abspath(sys.argv[0]))
        self._cp.read(os.path.join(self.cocos2d_path, "axmol.ini"))

        # XXX: override with local config ??? why ???
        # self._cp.read("~/.cocos2d-js/axmol.ini")

    def parse_plugins(self):
        classes = {}

        for s in self._cp.sections():
            if s == 'plugins':
                for classname in self._cp.options(s):
                    plugin_class = get_class(classname)
                    category = plugin_class.plugin_category()
                    name = plugin_class.plugin_name()
                    if name is None:
                        print(MultiLanguage.get_string('COCOS_PARSE_PLUGIN_WARNING_FMT', classname))
                    if len(category) == 0:
                        key = name
                    else:
                        # combine category & name as key
                        # eg. 'project_new'
                        key = category + '_' + name
                    classes[key] = plugin_class
        _check_dependencies(classes)
        return classes

    def _sanitize_path(self, path):
        if len(path) == 0:
            return None
        path = os.path.expanduser(path)
        path = os.path.abspath(os.path.join(self.cocos2d_path, path))
        if not os.path.isdir(path):
            Logging.warning(MultiLanguage.get_string('COCOS_WARNING_INVALID_DIR_IN_INI_FMT', path))
            return None
        return path

    def get_plugins_path(self):
        path = self._cp.get('paths', 'plugins')

        if not os.path.isabs(path):
            path = os.path.join(get_current_path(), path)

        path = self._sanitize_path(path)
        return path

    def get_cocos2dx_path(self):
        cocos2d_x = self._cp.get('paths', 'cocos2d_x')
        cocos2d_x = self._sanitize_path(cocos2d_x)
        return cocos2d_x

    def get_templates_path(self):
        templates = self._cp.get('paths', 'templates')
        templates = self._sanitize_path(templates)
        return templates

    def get_cocos2dx_mode(self):
        mode = self._cp.get('global', 'cocos2d_x_mode')
        if mode is None or len(mode) == 0:
            mode = 'source'

        if mode not in ('source', 'precompiled', 'distro'):
            Logging.warning(MultiLanguage.get_string('COCOS_WARNING_INVALID_MODE_FMT', mode))
            mode = 'source'

        return mode

    def is_statistic_enabled(self):
        try:
            ret = self._cp.getboolean('global', 'enable_stat')
        except:
            ret = True

        return ret


class Logging:
    # TODO maybe the right way to do this is to use something like colorama?
    RED = '\033[31m'
    GREEN = '\033[32m'
    YELLOW = '\033[33m'
    MAGENTA = '\033[35m'
    RESET = '\033[0m'

    @staticmethod
    def _print(s, color=None):
        if color and sys.stdout.isatty() and sys.platform != 'win32':
            print(color + s + Logging.RESET)
        else:
            print(s)

    @staticmethod
    def debug(s):
        Logging._print(s, Logging.MAGENTA)

    @staticmethod
    def info(s):
        Logging._print(s, Logging.GREEN)

    @staticmethod
    def warning(s):
        Logging._print(s, Logging.YELLOW)

    @staticmethod
    def error(s):
        Logging._print(s, Logging.RED)


class CCPluginError(Exception):
    ERROR_WRONG_ARGS = 11           # wrong arguments
    ERROR_PATH_NOT_FOUND = 12       # path not found
    ERROR_BUILD_FAILED = 13         # build failed
    ERROR_RUNNING_CMD = 14          # error when running command
    ERROR_CMD_NOT_FOUND = 15        # command not found
    ERROR_ENV_VAR_NOT_FOUND = 16    # environment variable not found
    ERROR_TOOLS_NOT_FOUND = 17      # depend on tools not found
    ERROR_PARSE_FILE = 18           # error when parse files
    ERROR_WRONG_CONFIG = 19         # configuration is wrong

    ERROR_OTHERS = 101              # other errors

    def __init__(self, err_args, err_no=1):
        super(CCPluginError, self).__init__(err_args)
        self.error_no = err_no

    def get_error_no(self):
        return self.error_no


class CMDRunner(object):

    @staticmethod
    def run_cmd(command, verbose, cwd=None):
        if verbose:
            Logging.debug(MultiLanguage.get_string('COCOS_DEBUG_RUNNING_CMD_FMT', ''.join(command)))
        else:
            log_path = CCPlugin._log_path()
            command += ' >"%s" 2>&1' % log_path
        sys.stdout.flush()
        ret = subprocess.call(command, shell=True, cwd=cwd)
        if ret != 0:
            message = MultiLanguage.get_string('COCOS_ERROR_RUNNING_CMD_RET_FMT', str(ret))
            if not verbose:
                message += (MultiLanguage.get_string('COCOS_ERROR_CHECK_LOG_FMT', log_path))
            raise CCPluginError(message, CCPluginError.ERROR_RUNNING_CMD)

    @staticmethod
    def output_for(command, verbose):
        if verbose:
            Logging.debug(MultiLanguage.get_string('COCOS_DEBUG_RUNNING_CMD_FMT', command))
        else:
            log_path = CCPlugin._log_path()

        try:
            return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
        except subprocess.CalledProcessError as e:
            output = e.output
            message = MultiLanguage.get_string('COCOS_ERROR_RUNNING_CMD')

            if not verbose:
                with open(log_path, 'w') as f:
                    f.write(output)
                message += MultiLanguage.get_string('COCOS_ERROR_CHECK_LOG_FMT', log_path)
            else:
                Logging.error(output)

            raise CCPluginError(message, CCPluginError.ERROR_RUNNING_CMD)

    @staticmethod
    def convert_path_to_cmd(path):
        """ Escape paths which include spaces to correct style which bash(mac) and cmd(windows) can treat correctly.

            eg: on mac: convert '/usr/xxx/apache-ant 1.9.3' to '/usr/xxx/apache-ant\ 1.9.3'
            eg: on windows: convert '"c:\apache-ant 1.9.3"\bin' to '"c:\apache-ant 1.9.3\bin"'
        """
        ret = path
        if os_is_mac():
            ret = path.replace("\ ", " ").replace(" ", "\ ")

        if os_is_win32():
            ret = "\"%s\"" % (path.replace("\"", ""))

        # print("!!!!! Convert %s to %s\n" % (path, ret))
        return ret

    @staticmethod
    def convert_path_to_python(path):
        """ Escape paths which include spaces to correct style which python can treat correctly.

            eg: on mac: convert '/usr/xxx/apache-ant\ 1.9.3' to '/usr/xxx/apache-ant 1.9.3'
            eg: on windows: convert '"c:\apache-ant 1.9.3"\bin' to 'c:\apache-ant 1.9.3\bin'
        """
        ret = path
        if os_is_mac():
            ret = path.replace("\ ", " ")

        if os_is_win32():
            ret = ret.replace("\"", "")

        # print("!!!!! Convert %s to %s\n" % (path, ret))
        return ret


class DataStatistic(object):
    '''
    In order to improve axmol, we periodically send anonymous data about how you use axmol.
    You can turn off this function by change the value of "enable_stat" in axmol.ini.

    Information collected will be used to develop new features and improve axmol.

    Since no personally identifiable information is collected,
    the anonymous data will not be meaningful to anyone outside of Chukong Inc.
    '''
    inited = False
    stat_obj = None
    key_last_state = 'last_stat_enabled'
    key_agreement_shown = 'agreement_shown'

    @classmethod
    def get_cfg_file_path(cls):
        return os.path.join(os.path.expanduser('~/.axmol'), 'local_cfg.json')

    @classmethod
    def get_cfg_value(cls, key, default_value):
        local_cfg_file = cls.get_cfg_file_path()
        if not os.path.isfile(local_cfg_file):
            cur_info = None
        else:
            try:
                f = open(local_cfg_file)
                cur_info = json.load(f)
                f.close()
            except:
                cur_info = None

        ret = default_value
        if cur_info is not None:
            if key in cur_info:
                ret = cur_info[key]

        return ret

    @classmethod
    def set_cfg_value(cls, key, value):
        # get current local config info
        cfg_file = cls.get_cfg_file_path()
        if not os.path.isfile(cfg_file):
            cur_info = {}
        else:
            try:
                f = open(cfg_file)
                cur_info = json.load(f)
                f.close()
            except:
                cur_info = {}

        # set the value in config
        cur_info[key] = value

        # make config directory if it's not already there
        cfg_dir = os.path.dirname(cfg_file)
        if not os.path.exists(cfg_dir):
            os.makedirs(cfg_dir)

        # write the config
        f = open(cfg_file, 'w')
        json.dump(cur_info, f, sort_keys=True, indent=4)
        f.close()

    # get the stat agreed or not
    @classmethod
    def is_agreement_shown(cls):
        return cls.get_cfg_value(cls.key_agreement_shown, False)

    @classmethod
    def change_agree_stat(cls, agreed):
        cls.set_cfg_value(cls.key_agreement_shown, True)

        # write the config to ini
        ini_file = os.path.join(get_current_path(), "axmol.ini")
        f = open(ini_file)
        old_lines = f.readlines()
        f.close()

        new_str = 'enable_stat=%s' % ('true' if agreed else 'false')
        new_lines = []
        for line in old_lines:
            new_line = re.sub('enable_stat[ \t]*=(.*)$', new_str, line)
            new_lines.append(new_line)

        f = open(ini_file, 'w', newline='\n')
        f.writelines(new_lines)
        f.close()

    @classmethod
    def show_stat_agreement(cls, skip_agree_value=None):
        if cls.is_agreement_shown():
            return

        if skip_agree_value is None:
            # show the agreement
            input_value = get_input(MultiLanguage.get_string('COCOS_AGREEMENT'))
            agreed = (input_value.lower() != 'n' and input_value.lower() != 'no')
        else:
            # --agreement is used to skip the input
            agreed = skip_agree_value
        cls.change_agree_stat(agreed)

    # change the last time statistics status in local config file.
    @classmethod
    def change_last_state(cls, enabled):
        cls.set_cfg_value(cls.key_last_state, enabled)

    # get the last time statistics status in local config file.
    @classmethod
    def get_last_state(cls):
        return cls.get_cfg_value(cls.key_last_state, True)

    @classmethod
    def init_stat_obj(cls):
        if cls.inited == False:
            # get the axmol_stat module
            m = None
            try:
                m = __import__("axmol_stat")
            except:
                pass

            if m is not None:
                stat_cls = getattr(m, "Statistic")
                cls.stat_obj = stat_cls(STAT_VERSION)

            # axmol_stat is found
            if cls.stat_obj is not None:
                # check config in axmol.ini
                parser = Cocos2dIniParser()
                cur_enabled = parser.is_statistic_enabled()

                # get last time is enabled or not
                last_enabled = cls.get_last_state()

                if not cur_enabled:
                    # statistics is disabled
                    if last_enabled:
                        cls.stat_obj.send_event('switch', 'off', 'stat_closed')
                    cls.stat_obj = None

                # update last time status
                if cur_enabled != last_enabled:
                    cls.change_last_state(cur_enabled)

            # try to send the cached events
            if cls.stat_obj is not None:
                cls.stat_obj.send_cached_events()

            cls.inited = True

        return cls.stat_obj

    @classmethod
    def stat_event(cls, category, action, label):
        try:
            cls.init_stat_obj()
            if cls.stat_obj is None:
                return

            cls.stat_obj.send_event(category, action, label)
        except:
            pass

    @classmethod
    def terminate_stat(cls):
        try:
            if cls.stat_obj is None:
                return

            cls.stat_obj.terminate_stat()
        except:
            pass


#
# Plugins should be a sublass of CCPlugin
#
class CCPlugin(object):

    def _run_cmd(self, command, cwd=None):
        CMDRunner.run_cmd(command, self._verbose, cwd)

    def _output_for(self, command):
        return CMDRunner.output_for(command, self._verbose)

    @classmethod
    def get_cocos2d_path(cls):
        """returns the path where axmol is installed"""

        #
        # 1: Check for config.ini
        #
        parser = Cocos2dIniParser()
        cocos2dx_path = parser.get_cocos2dx_path()

        if cocos2dx_path is not None:
            return cocos2dx_path

        #
        # 2: default engine path
        #
        # possible path of console
        # /Users/myself/axmol/tools/console/bin
        # if so, we have to remove the last 3 segments
        path = cls.get_console_path()
        path = os.path.abspath(path)
        cocos2dx_path = os.path.abspath(os.path.join(
            path, os.path.pardir, os.path.pardir, os.path.pardir))
        if os.path.isdir(cocos2dx_path):
            return cocos2dx_path

        if cls.get_cocos2d_mode() != "distro": # if cls.get_cocos2d_mode() is not "distro":
            # In 'distro' mode this is not a warning since
            # the source code is not expected to be installed
            Logging.warning(MultiLanguage.get_string('COCOS_WARNING_ENGINE_NOT_FOUND'))
        return None

    @classmethod
    def get_console_path(cls):
        """returns the path where cocos console is installed"""
        run_path = encode_with(get_current_path(), "utf-8")
        return run_path

    @classmethod
    def get_templates_paths(cls):
        """returns a set of paths where templates are installed"""

        parser = Cocos2dIniParser()
        templates_path = parser.get_templates_path()

        paths = []

        #
        # 1: Check for config.ini
        #
        if templates_path is not None:
            paths.append(templates_path)

        #
        # 2: Path defined by walking the cocos2d path
        #
        path = cls.get_cocos2d_path()

        if path is not None:
            # Try one: cocos2d-x/templates (assuming it is using cocos2d-x's setup.py)
            # Try two: cocos2d-x/../../templates
            possible_paths = [['templates'], ['..', '..', 'templates']]
            for p in possible_paths:
                p = str_join(os.sep, p)
                template_path = os.path.abspath(os.path.join(path, p))
                try:
                    if os.path.isdir(template_path):
                        paths.append(template_path)
                except Exception as e:
                    Logging.info(MultiLanguage.get_string('COCOS_INFO_CHECK_TEMPLATE_PATH_FAILED_FMT', template_path))
                    Logging.info("%s" % e)
                    pass

        #
        # 3: Templates can be in ~/.cocos2d/templates as well
        #
        user_path = os.path.expanduser("~/.axmol/templates")
        if os.path.isdir(user_path):
            paths.append(user_path)

        if len(paths) == 0:
            raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_TEMPLATE_NOT_FOUND'),
                                CCPluginError.ERROR_PATH_NOT_FOUND)

        # remove duplicates
        from collections import OrderedDict
        ordered = OrderedDict.fromkeys(paths)
        paths = ordered.keys()
        return paths

    @classmethod
    def get_cocos2d_mode(cls):
        parser = Cocos2dIniParser()
        return parser.get_cocos2dx_mode()

    @staticmethod
    def _log_path():
        log_dir = os.path.expanduser("~/.axmol")
        if not os.path.exists(log_dir):
            os.mkdir(log_dir)
        return os.path.join(log_dir, "axmol.log")

    # the list of plugins this plugin needs to run before itself.
    # ie: if it returns ('a', 'b'), the plugin 'a' will run first, then 'b'
    # and after that, the plugin itself.
    # they all share the same command line arguments
    @staticmethod
    def depends_on():
        return None

    # returns the plugin category,
    # default is empty string.
    @staticmethod
    def plugin_category():
        return ""

    # returns the plugin name
    @staticmethod
    def plugin_name():
        pass

    # returns help
    @staticmethod
    def brief_description():
        pass

    # Constructor
    def __init__(self):
        pass

    # Setup common options. If a subclass needs custom options,
    # override this method and call super.
    def init(self, args):
        self._verbose = (not args.quiet)
        self._platforms = axmol_project.Platforms(self._project, args.platform, args.proj_dir)
        if self._platforms.none_active():
            self._platforms.select_one()

    # Run it
    def run(self, argv, dependencies):
        pass

    # If a plugin needs to add custom parameters, override this method.
    # There's no need to call super
    def _add_custom_options(self, parser):
        pass

    # If a plugin needs to check custom parameters values after parsing them,
    # override this method.
    # There's no need to call super
    def _check_custom_options(self, args):
        pass

    def parse_args(self, argv):
        from argparse import ArgumentParser

        # FIXME:
        # CCPlugin should not parse any argument. Plugins are responsoble for doing it
        parser = ArgumentParser(prog="cocos %s" % self.__class__.plugin_name(),
                                description=self.__class__.brief_description())
        parser.add_argument("-s", "--src",
                            dest="src_dir",
                            help=MultiLanguage.get_string('COCOS_HELP_ARG_SRC'))
        parser.add_argument("-q", "--quiet",
                            action="store_true",
                            dest="quiet",
                            help=MultiLanguage.get_string('COCOS_HELP_ARG_QUIET'))
        platform_list = axmol_project.Platforms.list_for_display()
        parser.add_argument("-p", "--platform",
                            dest="platform",
                            help=MultiLanguage.get_string('COCOS_HELP_ARG_PLATFORM'))
        parser.add_argument("--list-platforms",
                            action="store_true",
                            dest="listplatforms",
                            help=_("List available platforms"))
        parser.add_argument("--proj-dir",
                            dest="proj_dir",
                            help=MultiLanguage.get_string('COCOS_HELP_ARG_PROJ_DIR'))
        self._add_custom_options(parser)

        (args, unkonw) = parser.parse_known_args(argv)

        if args.src_dir is None:
            self._project = axmol_project.Project(os.path.abspath(os.getcwd()))
        else:
            self._project = axmol_project.Project(
                os.path.abspath(args.src_dir))

        args.src_dir = self._project.get_project_dir()
        if args.src_dir is None:
            raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_PROJECT_NOT_FOUND'),
                                CCPluginError.ERROR_WRONG_ARGS)

        if args.platform:
            args.platform = args.platform.lower()
            if args.platform not in platform_list:
                raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_UNKNOWN_PLATFORM_FMT', args.platform),
                                    CCPluginError.ERROR_WRONG_ARGS)

        if args.listplatforms and self._project is not None:
            platforms = axmol_project.Platforms(self._project, args.platform, args.proj_dir)
            p = list(platforms.get_available_platforms().keys())
            print('{"platforms":' + json.dumps(p) + '}')
            sys.exit(0)

        self.init(args)
        self._check_custom_options(args)


def get_current_path():
    if getattr(sys, 'frozen', None):
        ret = os.path.realpath(os.path.dirname(sys.executable))
    else:
        ret = os.path.realpath(os.path.dirname(__file__))

    return ret


# get_class from: http://stackoverflow.com/a/452981
def get_class(kls):
    parts = kls.split('.')
    module = ".".join(parts[:-1])
    if len(parts) == 1:
        m = sys.modules[__name__]
        m = getattr(m, parts[0])
    else:
        m = __import__(module)
        for comp in parts[1:]:
            m = getattr(m, comp)
    return m


def _check_dependencies_exist(dependencies, classes, plugin_name):
    for dep in dependencies:
        if dep not in classes:
            raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_INVALID_DEPENDENCY_FMT',
                                (plugin_name, dep)),
                                CCPluginError.ERROR_CMD_NOT_FOUND)


def _check_dependencies(classes):
    for k in classes:
        plugin = classes[k]
        dependencies = plugin.depends_on()
        if dependencies is not None:
            _check_dependencies_exist(dependencies, classes, k)


# common functions

def check_environment_variable(var):
    ''' Checking the environment variable, if found then return it's value, else raise error
    '''
    try:
        value = os.environ[var]
    except Exception:
        raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_ENV_NOT_DEFINED_FMT', var),
                            CCPluginError.ERROR_ENV_VAR_NOT_FOUND)

    return value


def get_xcode_version():
    commands = [
        "xcodebuild",
        "-version"
    ]
    child = subprocess.Popen(commands, stdout=subprocess.PIPE)

    xcode = None
    version = None
    for line in child.stdout:
        line = line.decode('utf8')

        if 'Xcode' in line:
            xcode, version = str.split(line, ' ')

    child.wait()

    if xcode is None:
        raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_XCODE_NOT_INSTALLED'),
                            CCPluginError.ERROR_TOOLS_NOT_FOUND)

    return version

def app_is_installed(adb_cmd, pack_name):
    list_pack_cmd = "%s shell 'pm list packages'" % (adb_cmd)
    desired_name = "package:%s" % (pack_name)
    child = subprocess.Popen(list_pack_cmd, stdout=subprocess.PIPE, shell=True)
    for line in child.stdout:
        if desired_name == line.strip():
            return True
    return False

def version_compare(a, op, b):
    '''Compares two version numbers to see if a op b is true

    op is operator
    op can be ">", "<", "==", "!=", ">=", "<="
    a and b are version numbers (dot separated)
    a and b can be string, float or int

    Please note that: 3 == 3.0 == 3.0.0 ... ("==" is not a simple string cmp)
    '''
    allowed = [">", "<", "==", "!=", ">=", "<="]
    if op not in allowed:
        raise ValueError("op must be one of {}".format(allowed))

    # Use recursion to simplify operators:
    if op[0] == "<": # Reverse args and inequality sign:
        return version_compare(b, op.replace("<",">"), a)
    if op == ">=":
        return version_compare(a,"==",b) or version_compare(a,">",b)
    if op == "!=":
        return not version_compare(a,"==",b)

    # We now have 1 of 2 base cases, "==" or ">":
    assert op in ["==", ">"]

    a = [int(x) for x in str(a).split(".")]
    b = [int(x) for x in str(b).split(".")]

    for i in range(max(len(a), len(b))):
        ai, bi = 0, 0   # digits
        if len(a) > i:
            ai = a[i]
        if len(b) > i:
            bi = b[i]
        if ai > bi:
            if op == ">":
                return True
            else: # op "=="
                return False
        if ai < bi:
            # Both "==" and ">" are False:
            return False
    if op == ">":
        return False    # op ">" and all digits were equal
    return True         # op "==" and all digits were equal

def copy_files_in_dir(src, dst):

    for item in os.listdir(src):
        path = os.path.join(src, item)
        if os.path.isfile(path):
            path = add_path_prefix(path)
            copy_dst = add_path_prefix(dst)
            shutil.copy(path, copy_dst)
        if os.path.isdir(path):
            new_dst = os.path.join(dst, item)
            if not os.path.isdir(new_dst):
                os.makedirs(add_path_prefix(new_dst))
            copy_files_in_dir(path, new_dst)


def copy_files_with_config(config, src_root, dst_root):
    src_dir = config["from"]
    dst_dir = config["to"]

    src_dir = os.path.join(src_root, src_dir)
    dst_dir = os.path.join(dst_root, dst_dir)

    include_rules = None
    if "include" in config:
        include_rules = config["include"]
        include_rules = convert_rules(include_rules)

    exclude_rules = None
    if "exclude" in config:
        exclude_rules = config["exclude"]
        exclude_rules = convert_rules(exclude_rules)

    copy_files_with_rules(
        src_dir, src_dir, dst_dir, include_rules, exclude_rules)


def copy_files_with_rules(src_rootDir, src, dst, include=None, exclude=None):
    if os.path.isfile(src):
        if not os.path.exists(dst):
            os.makedirs(add_path_prefix(dst))

        copy_src = add_path_prefix(src)
        copy_dst = add_path_prefix(dst)
        shutil.copy(copy_src, copy_dst)
        return

    if (include is None) and (exclude is None):
        if not os.path.exists(dst):
            os.makedirs(add_path_prefix(dst))
        copy_files_in_dir(src, dst)
    elif (include is not None):
        # have include
        for name in os.listdir(src):
            abs_path = os.path.join(src, name)
            rel_path = os.path.relpath(abs_path, src_rootDir)
            if os.path.isdir(abs_path):
                sub_dst = os.path.join(dst, name)
                copy_files_with_rules(
                    src_rootDir, abs_path, sub_dst, include=include)
            elif os.path.isfile(abs_path):
                if _in_rules(rel_path, include):
                    if not os.path.exists(dst):
                        os.makedirs(add_path_prefix(dst))

                    abs_path = add_path_prefix(abs_path)
                    copy_dst = add_path_prefix(dst)
                    shutil.copy(abs_path, copy_dst)
    elif (exclude is not None):
        # have exclude
        for name in os.listdir(src):
            abs_path = os.path.join(src, name)
            rel_path = os.path.relpath(abs_path, src_rootDir)
            if os.path.isdir(abs_path):
                sub_dst = os.path.join(dst, name)
                copy_files_with_rules(
                    src_rootDir, abs_path, sub_dst, exclude=exclude)
            elif os.path.isfile(abs_path):
                if not _in_rules(rel_path, exclude):
                    if not os.path.exists(dst):
                        os.makedirs(add_path_prefix(dst))

                    abs_path = add_path_prefix(abs_path)
                    copy_dst = add_path_prefix(dst)
                    shutil.copy(abs_path, copy_dst)


def _in_rules(rel_path, rules):
    ret = False
    path_str = rel_path.replace("\\", "/")
    for rule in rules:
        if re.match(rule, path_str):
            ret = True

    return ret


def convert_rules(rules):
    ret_rules = []
    for rule in rules:
        ret = rule.replace('.', '\\.')
        ret = ret.replace('*', '.*')
        ret = "%s" % ret
        ret_rules.append(ret)

    return ret_rules


def os_is_win32():
    return sys.platform == 'win32'


def os_is_32bit_windows():
    if not os_is_win32():
        return False

    arch = os.environ['PROCESSOR_ARCHITECTURE'].lower()
    archw = "PROCESSOR_ARCHITEW6432" in os.environ
    return (arch == "x86" and not archw)


def os_is_mac():
    return sys.platform == 'darwin'


def os_is_linux():
    return 'linux' in sys.platform


def add_path_prefix(path_str):
    if not os_is_win32():
        return path_str

    if path_str.startswith("\\\\?\\"):
        return path_str

    ret = "\\\\?\\" + os.path.abspath(path_str)
    ret = ret.replace("/", "\\")
    return ret


# get from http://stackoverflow.com/questions/6194499/python-os-system-pushd
@contextmanager
def pushd(newDir):
    previousDir = os.getcwd()
    os.chdir(newDir)
    yield
    os.chdir(previousDir)


def help():
    print(MultiLanguage.get_string('COCOS_HELP_BRIEF_FMT',
          (sys.argv[0], AXOML_CONSOLE_VERSION)))
    print(MultiLanguage.get_string('COCOS_HELP_AVAILABLE_CMD'))
    parse = Cocos2dIniParser()
    classes = parse.parse_plugins()
    max_name = max(len(classes[key].plugin_name(
    ) + classes[key].plugin_category()) for key in classes.keys())
    max_name += 4
    for key in classes.keys():
        plugin_class = classes[key]
        category = plugin_class.plugin_category()
        category = (category + ' ') if len(category) > 0 else ''
        name = plugin_class.plugin_name()
        print("\t%s%s%s%s" % (category, name,
                              ' ' * (max_name - len(name + category)),
                              plugin_class.brief_description()))

    print(MultiLanguage.get_string('COCOS_HELP_AVAILABLE_ARGS_FMT',
                                   MultiLanguage.get_available_langs()))
    print(MultiLanguage.get_string('COCOS_HELP_EXAMPLE'))

def show_version():
    print(AXOML_ENGINE_VERSION)
    print("Axmol Console %s" % AXOML_CONSOLE_VERSION)

def run_plugin(command, argv, plugins):
    run_directly = False
    if len(argv) > 0:
        if argv[0] in ['--help', '-h']:
            run_directly = True

    plugin = plugins[command]()

    if run_directly:
        plugin.run(argv, None)
    else:
        dependencies = plugin.depends_on()
        dependencies_objects = {}
        if dependencies is not None:
            for dep_name in dependencies:
                # FIXME check there's not circular dependencies
                dependencies_objects[dep_name] = run_plugin(
                    dep_name, argv, plugins)
        # don't print this info. Not useful to users, and generates noise when parsing output
#        Logging.info(MultiLanguage.get_string('COCOS_INFO_RUNNING_PLUGIN_FMT', plugin.__class__.plugin_name()))
        plugin.run(argv, dependencies_objects)
        return plugin


def _check_python_version():
    major_ver = sys.version_info[0]
    minor_ver = sys.version_info[1]
    ret = True
    if major_ver < 2:
        ret = False
    elif minor_ver < 7:
        ret = False

    if not ret:
        print(MultiLanguage.get_string('COCOS_PYTHON_VERSION_TIP_FMT') % (major_ver, minor_ver))

    return ret

# gettext
language = None
encoding = None
try:
    locale.setlocale(locale.LC_ALL, '')  # use user's preferred locale
    language, encoding = locale.getlocale()
except:
    pass

if language is not None:
    filename = "language_%s.mo" % language[0:2]
    try:
        trans = gettext.GNUTranslations(open(filename, "rb"))
    except IOError:
        trans = gettext.NullTranslations()
    trans.install()
    _ = trans.gettext
else:
    _ = MultiLanguage.get_string

if __name__ == "__main__":
    # Parse the arguments, specify the language
    language_arg = '--ol'
    if language_arg in sys.argv:
        idx = sys.argv.index(language_arg)
        if idx == (len(sys.argv) - 1):
            Logging.error(MultiLanguage.get_string('COCOS_ERROR_OL_NO_VALUE'))
            sys.exit(CCPluginError.ERROR_WRONG_ARGS)

        # set specified language
        MultiLanguage.set_language(sys.argv[idx+1])

        # remove the argument '--ol' & the value
        sys.argv.pop(idx)
        sys.argv.pop(idx)

    agreement_arg = '--agreement'
    skip_agree_value = None
    if agreement_arg in sys.argv:
        idx = sys.argv.index(agreement_arg)
        if idx == (len(sys.argv) - 1):
            Logging.error(MultiLanguage.get_string('COCOS_ERROR_AGREEMENT_NO_VALUE'))
            sys.exit(CCPluginError.ERROR_WRONG_ARGS)

        # get the argument value
        agree_value = sys.argv[idx+1]
        if agree_value.lower() == 'n':
            skip_agree_value = False
        else:
            skip_agree_value = True

        # remove the argument '--agreement' & the value
        sys.argv.pop(idx)
        sys.argv.pop(idx)

    # Get the engine version for the DataStat
    cur_path = get_current_path()
    engine_path = os.path.normpath(os.path.join(cur_path, '../../../'))
    AXOML_ENGINE_VERSION = utils.get_engine_version(engine_path)
    print(AXOML_ENGINE_VERSION)
    STAT_VERSION = AXOML_ENGINE_VERSION
    ver_pattern = r"axmol-(.*)"
    match = re.match(ver_pattern, AXOML_ENGINE_VERSION)
    if match:
        STAT_VERSION = match.group(1)

    force_disable_stats = True # Temp disable stats
    if force_disable_stats:
        DataStatistic.change_agree_stat(False)
    else:
        DataStatistic.show_stat_agreement(skip_agree_value)
        DataStatistic.stat_event('axmol', 'start', 'invoked')

    if not _check_python_version():
        DataStatistic.terminate_stat()
        sys.exit(CCPluginError.ERROR_TOOLS_NOT_FOUND)

    parser = Cocos2dIniParser()
    plugins_path = parser.get_plugins_path()
    sys.path.append(plugins_path)

    if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help'):
        help()
        DataStatistic.terminate_stat()
        sys.exit(0)

    if len(sys.argv) > 1 and sys.argv[1] in ('-v', '--version'):
        show_version()
        DataStatistic.terminate_stat()
        sys.exit(0)

    try:
        plugins = parser.parse_plugins()
        command = sys.argv[1]
        argv = sys.argv[2:]
        # try to find plugin by name
        if command in plugins:
            DataStatistic.stat_event('axmol', 'running_command', command)
            run_plugin(command, argv, plugins)
        else:
            # try to find plugin by category_name, so the len(sys.argv) at
            # least 3.
            if len(sys.argv) > 2:
                # combine category & name as key
                # eg. 'project_new'
                command = sys.argv[1] + '_' + sys.argv[2]
                argv = sys.argv[3:]
                if command in plugins:
                    DataStatistic.stat_event('axmol', 'running_command', command)
                    run_plugin(command, argv, plugins)
                else:
                    raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_CMD_NOT_FOUND_FMT',
                                                                 ' '.join(sys.argv[1:])),
                                        CCPluginError.ERROR_CMD_NOT_FOUND)
            else:
                raise CCPluginError(MultiLanguage.get_string('COCOS_ERROR_CMD_NOT_FOUND_FMT', command),
                                    CCPluginError.ERROR_CMD_NOT_FOUND)

    except Exception as e:
        # FIXME don't know how to handle this. Can't catch cocos2d.CCPluginError
        # as it's not defined that way in this file, but the plugins raise it
        # with that name.
        if e.__class__.__name__ == 'CCPluginError':
            Logging.error(' '.join(e.args))
            # import traceback
            # print '-' * 60
            # traceback.print_exc(file=sys.stdout)
            # print '-' * 60
            err_no = e.get_error_no()
            sys.exit(err_no)
        else:
            raise
    finally:
        DataStatistic.terminate_stat()