import os
import re
import json
import axmol
from MultiLanguage import MultiLanguage

class Project(object):
    CPP = 'cpp'
    LUA = 'lua'
    JS = 'js'

    CONFIG = '.axproj.json'

    KEY_PROJ_TYPE = 'project_type'
    KEY_HAS_NATIVE = 'has_native'
    KEY_CUSTOM_STEP_SCRIPT = "custom_step_script"
    KEY_ENGINE_VERSION = "engine_version"

    CUSTOM_STEP_PRE_BUILD        = "pre-build"
    CUSTOM_STEP_POST_BUILD       = "post-build"
    CUSTOM_STEP_PRE_NDK_BUILD    = "pre-ndk-build"
    CUSTOM_STEP_POST_NDK_BUILD   = "post-ndk-build"
    CUSTOM_STEP_PRE_COPY_ASSETS  = "pre-copy-assets"
    CUSTOM_STEP_POST_COPY_ASSETS = "post-copy-assets"
    CUSTOM_STEP_PRE_ANT_BUILD    = "pre-ant-build"
    CUSTOM_STEP_POST_ANT_BUILD   = "post-ant-build"

    @staticmethod
    def list_for_display():
        return [x.lower() for x in Project.language_list()]

    @staticmethod
    def language_list():
        return (Project.CPP, Project.LUA, Project.JS)

    def __init__(self, project_dir):
        # parse the config file
        self.info = self._parse_project_json(project_dir)

    def _parse_project_json(self, src_dir):
        proj_path = self._find_project_dir(src_dir)
        # config file is not found
        if proj_path == None:
            raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_CFG_NOT_FOUND_FMT',
                                      os.path.join(src_dir, Project.CONFIG)),
                                      axmol.CCPluginError.ERROR_PATH_NOT_FOUND)

        project_json = os.path.join(proj_path, Project.CONFIG)
        try:
            f = open(project_json)
            project_info = json.load(f)
            f.close()
        except Exception:
            if f is not None:
                f.close()
            raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_CFG_BROKEN_FMT',
                                      project_json),
                                      axmol.CCPluginError.ERROR_PARSE_FILE)

        if project_info is None:
            raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_CFG_PARSE_FAILED_FMT',
                                      Project.CONFIG), axmol.CCPluginError.ERROR_PARSE_FILE)

        if not axmol.dict_contains(project_info, Project.KEY_PROJ_TYPE):
            raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_CFG_GET_VALUE_FAILED_FMT',
                                      (Project.KEY_PROJ_TYPE, Project.CONFIG)),
                                      axmol.CCPluginError.ERROR_WRONG_CONFIG)

        lang = project_info[Project.KEY_PROJ_TYPE]
        lang = lang.lower()

        # The config is invalid
        if not (lang in Project.language_list()):
            raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_CFG_INVALID_LANG_FMT',
                                      (Project.KEY_PROJ_TYPE, ', '.join(Project.list_for_display()))),
                                      axmol.CCPluginError.ERROR_WRONG_CONFIG)

        # record the dir & language of the project
        self._project_dir = proj_path
        self._project_lang = lang

        # if is script project, record whether it has native or not
        self._has_native = False
        if (self._is_script_project() and axmol.dict_contains(project_info, Project.KEY_HAS_NATIVE)):
            self._has_native = project_info[Project.KEY_HAS_NATIVE]

        # if has custom step script, record it
        self._custom_step = None
        if (axmol.dict_contains(project_info, Project.KEY_CUSTOM_STEP_SCRIPT)):
            script_path = project_info[Project.KEY_CUSTOM_STEP_SCRIPT]
            if not os.path.isabs(script_path):
                script_path = os.path.join(self._project_dir, script_path)

            if os.path.isfile(script_path):
                import sys
                script_dir, script_name = os.path.split(script_path)
                sys.path.append(script_dir)
                self._custom_step = __import__(os.path.splitext(script_name)[0])
                axmol.Logging.info(MultiLanguage.get_string('PROJECT_INFO_FOUND_CUSTOM_STEP_FMT', script_path))
            else:
                axmol.Logging.warning(MultiLanguage.get_string('PROJECT_WARNING_CUSTOM_SCRIPT_NOT_FOUND_FMT',
                                      script_path))
                self._custom_step = None

        return project_info

    def invoke_custom_step_script(self, event, tp, args):
        try:
            if self._custom_step is not None:
                self._custom_step.handle_event(event, tp, args)
        except Exception as e:
            axmol.Logging.warning(MultiLanguage.get_string('PROJECT_WARNING_CUSTOM_STEP_FAILED_FMT', e))
            raise e

    def _find_project_dir(self, start_path):
        path = start_path
        while True:
            if axmol.os_is_win32():
                # windows root path, eg. c:\
                if re.match(".+:\\\\$", path):
                    break
            else:
                # unix like use '/' as root path
                if path == '/' :
                    break
            cfg_path = os.path.join(path, Project.CONFIG)
            if (os.path.exists(cfg_path) and os.path.isfile(cfg_path)):
                return path

            path = os.path.dirname(path)

        return None

    def get_proj_config(self, key):
        project_json = os.path.join(self._project_dir, Project.CONFIG)
        f = open(project_json)
        project_info = json.load(f)
        f.close()

        ret = None
        if axmol.dict_contains(project_info, key):
            ret = project_info[key]

        return ret

    def write_proj_config(self, key, value):
        project_json = os.path.join(self._project_dir, Project.CONFIG)

        if os.path.isfile(project_json):
            f = open(project_json)
            project_info = json.load(f)
            f.close()

        if project_info is None:
            project_info = {}

        project_info[key] = value

        outfile = open(project_json, "w")
        json.dump(project_info, outfile, sort_keys = True, indent = 4)
        outfile.close()

    def get_project_dir(self):
        return self._project_dir

    def get_language(self):
        return self._project_lang

    def has_android_libs(self):
        if self._is_script_project():
            proj_android_path = os.path.join(self.get_project_dir(), "frameworks", "runtime-src", "proj.android", "libs")
        else:
            proj_android_path = os.path.join(self.get_project_dir(), "proj.android", "libs")

        return os.path.isdir(proj_android_path)

    def _is_native_support(self):
        return self._has_native

    def _is_script_project(self):
        return self._is_lua_project() or self._is_js_project()

    def _is_cpp_project(self):
        return self._project_lang == Project.CPP

    def _is_lua_project(self):
        return self._project_lang == Project.LUA

    def _is_js_project(self):
        return self._project_lang == Project.JS

class Platforms(object):
    ANDROID = 'android'
    IOS = 'ios'
    TVOS = 'tvos'
    MAC = 'mac'
    WEB = 'web'
    WIN32 = 'win32'
    LINUX = 'linux'

    CFG_CLASS_MAP = {
        ANDROID : "axmol_project.AndroidConfig",
        IOS : "axmol_project.iOSConfig",
        TVOS : "axmol_project.tvOSConfig",
        MAC : "axmol_project.MacConfig",
        WEB : "axmol_project.WebConfig",
        WIN32 : "axmol_project.Win32Config",
        LINUX : "axmol_project.LinuxConfig",
    }

    @staticmethod
    def list_for_display():
        return [x.lower() for x in Platforms.list()]

    @staticmethod
    def list():
        return Platforms.CFG_CLASS_MAP.keys()

    def __init__(self, project, current, proj_dir = None):
        self._project = project

        proj_info = self._project.info
        self._gen_available_platforms(proj_info, proj_dir)

        self._current = None
        if current is not None:
            current_lower = current.lower()
            if current_lower in self._available_platforms.keys():
                self._current = current_lower
            else:
                raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_INVALID_PLATFORM_FMT',
                                          (self._available_platforms.keys(), current)),
                                          axmol.CCPluginError.ERROR_WRONG_ARGS)

    def _filter_platforms(self, platforms):
        ret = []
        platforms_for_os = {
            "linux" : [ Platforms.WEB, Platforms.LINUX, Platforms.ANDROID ],
            "mac" : [ Platforms.WEB, Platforms.IOS, Platforms.TVOS, Platforms.MAC, Platforms.ANDROID ],
            "win32" : [ Platforms.WEB, Platforms.WIN32, Platforms.ANDROID ]
        }
        for p in platforms:
            if axmol.os_is_linux():
                if p in platforms_for_os["linux"]:
                    ret.append(p)
            if axmol.os_is_mac():
                if p in platforms_for_os["mac"]:
                    ret.append(p)
            if axmol.os_is_win32():
                if p in platforms_for_os["win32"]:
                    ret.append(p)

        return ret

    def _gen_available_platforms(self, proj_info, proj_dir):
        # generate the platform list for different projects
        if self._project._is_lua_project():
            if self._project._is_native_support():
                platform_list = [ Platforms.ANDROID, Platforms.WIN32, Platforms.IOS, Platforms.TVOS, Platforms.MAC, Platforms.LINUX ]
            else:
                if self._project.has_android_libs():
                    platform_list = [ Platforms.ANDROID ]
                else:
                    platform_list = []
        elif self._project._is_js_project():
            if self._project._is_native_support():
                platform_list = [ Platforms.ANDROID, Platforms.WIN32, Platforms.IOS, Platforms.TVOS, Platforms.MAC, Platforms.WEB, Platforms.LINUX ]
            else:
                if self._project.has_android_libs():
                    platform_list = [ Platforms.ANDROID, Platforms.WEB ]
                else:
                    platform_list = [ Platforms.WEB ]
        elif self._project._is_cpp_project():
            platform_list = [ Platforms.ANDROID, Platforms.WIN32, Platforms.IOS, Platforms.TVOS, Platforms.MAC, Platforms.LINUX ]

        # filter the available platform list
        platform_list = self._filter_platforms(platform_list)

        # check the real available platforms
        self._available_platforms = {}
        root_path = self._project.get_project_dir()
        for p in platform_list:
            cfg_class = axmol.get_class(Platforms.CFG_CLASS_MAP[p])
            if cfg_class is None:
                continue

            cfg_key = "%s_cfg" % p
            if axmol.dict_contains(proj_info, cfg_key):
                cfg_obj = cfg_class(root_path, self._project._is_script_project(), proj_info[cfg_key])
            else:
                cfg_obj = cfg_class(root_path, self._project._is_script_project())

            if proj_dir is not None:
                cfg_obj.proj_path = os.path.join(root_path, proj_dir)

            if cfg_obj._is_available():
                self._available_platforms[p] = cfg_obj

        # don't have available platforms
        if len(self._available_platforms) == 0:
            raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_NO_AVAILABLE_PLATFORMS'),
                                      axmol.CCPluginError.ERROR_WRONG_CONFIG)

    def get_current_platform(self):
        return self._current

    def get_available_platforms(self):
        return self._available_platforms

    def none_active(self):
        return self._current is None

    def is_android_active(self):
        return self._current == Platforms.ANDROID

    def is_ios_active(self):
        return self._current == Platforms.IOS

    def is_tvos_active(self):
        return self._current == Platforms.TVOS

    def is_mac_active(self):
        return self._current == Platforms.MAC

    def is_web_active(self):
        return self._current == Platforms.WEB

    def is_win32_active(self):
        return self._current == Platforms.WIN32

    def is_linux_active(self):
        return self._current == Platforms.LINUX

    def get_current_config(self):
        if self.none_active():
            return None

        return self._available_platforms[self._current]

    def project_path(self):
        if self._current is None:
            return None

        cfg_obj = self._available_platforms[self._current]
        return cfg_obj.proj_path

    def _has_one(self):
        return len(self._available_platforms) == 1

    def select_one(self):
        if self._has_one():
            self._current = self._available_platforms.keys()[0]
            return

        raise axmol.CCPluginError(MultiLanguage.get_string('PROJECT_SPECIFY_PLATFORM_FMT',
                                  str(self._available_platforms.keys())),
                                  axmol.CCPluginError.ERROR_WRONG_CONFIG)

class PlatformConfig(object):
    KEY_PROJ_PATH = "project_path"
    def __init__(self, proj_root_path, is_script, cfg_info = None):
        self._proj_root_path = proj_root_path
        self._is_script = is_script
        if cfg_info is None:
            self._use_default()
        else:
            self._parse_info(cfg_info)

    def _use_default(self):
        pass

    def _parse_info(self, cfg_info):
        if axmol.dict_contains(cfg_info, PlatformConfig.KEY_PROJ_PATH):
            self.proj_path = os.path.join(self._proj_root_path, cfg_info[PlatformConfig.KEY_PROJ_PATH])
        else:
            self.proj_path = None

    def _is_available(self):
        ret = True
        if self.proj_path is None or not os.path.isdir(self.proj_path):
            ret = False

        return ret

class AndroidConfig(PlatformConfig):

    def _use_default(self):
        if self._is_script:
            self.proj_path = os.path.join(self._proj_root_path, "frameworks", "runtime-src", "proj.android")
        else:
            self.proj_path = os.path.join(self._proj_root_path, "proj.android")

    def _parse_info(self, cfg_info):
        super(AndroidConfig, self)._parse_info(cfg_info)

    def _is_available(self):
        proj_android_existed = super(AndroidConfig, self)._is_available()
        return proj_android_existed

class LinuxConfig(PlatformConfig):
    KEY_CMAKE_PATH = "cmake_path"
    KEY_BUILD_DIR = "build_dir"
    KEY_PROJECT_NAME = "project_name"
    KEY_BUILD_RESULT_DIR = "build_result_dir"

    def _use_default(self):
        if self._is_script:
            self.proj_path = os.path.join(self._proj_root_path, "frameworks", "runtime-src", "proj.linux")
        else:
            self.proj_path = os.path.join(self._proj_root_path, "proj.linux")

        self.cmake_path = None
        self.build_dir = None
        self.project_name = None
        self.build_result_dir = None

    def _parse_info(self, cfg_info):
        super(LinuxConfig, self)._parse_info(cfg_info)
        if axmol.dict_contains(cfg_info, LinuxConfig.KEY_CMAKE_PATH):
            self.cmake_path = cfg_info[LinuxConfig.KEY_CMAKE_PATH]
        else:
            self.cmake_path = None

        if axmol.dict_contains(cfg_info, LinuxConfig.KEY_BUILD_DIR):
            self.build_dir = cfg_info[LinuxConfig.KEY_BUILD_DIR]
        else:
            self.build_dir = None

        if axmol.dict_contains(cfg_info, LinuxConfig.KEY_PROJECT_NAME):
            self.project_name = cfg_info[LinuxConfig.KEY_PROJECT_NAME]
        else:
            self.project_name = None

        if axmol.dict_contains(cfg_info, LinuxConfig.KEY_BUILD_RESULT_DIR):
            self.build_result_dir = cfg_info[LinuxConfig.KEY_BUILD_RESULT_DIR]
        else:
            self.build_result_dir = None

    def _is_available(self):
        ret = super(LinuxConfig, self)._is_available()

        return ret

class MacConfig(LinuxConfig):
    def _use_default(self):
        super(MacConfig, self)._use_default()

        if self._is_script:
            self.proj_path = os.path.join(self._proj_root_path, "frameworks", "runtime-src", "proj.ios_mac")
        else:
            self.proj_path = os.path.join(self._proj_root_path, "proj.ios_mac")

class iOSConfig(LinuxConfig):
    def _use_default(self):
        super(iOSConfig, self)._use_default()

        if self._is_script:
            self.proj_path = os.path.join(self._proj_root_path, "frameworks", "runtime-src", "proj.ios_mac")
        else:
            self.proj_path = os.path.join(self._proj_root_path, "proj.ios_mac")

class tvOSConfig(LinuxConfig):
    def _use_default(self):
        super(tvOSConfig, self)._use_default()

        if self._is_script:
            self.proj_path = os.path.join(self._proj_root_path, "frameworks", "runtime-src", "proj.ios_mac")
        else:
            self.proj_path = os.path.join(self._proj_root_path, "proj.ios_mac")

class Win32Config(LinuxConfig):
    def _use_default(self):
        super(Win32Config, self)._use_default()

        if self._is_script:
            self.proj_path = os.path.join(self._proj_root_path, "frameworks", "runtime-src", "proj.win32")
        else:
            self.proj_path = os.path.join(self._proj_root_path, "proj.win32")

class WebConfig(PlatformConfig):
    KEY_SUB_URL = "sub_url"
    KEY_RUN_ROOT_DIR = "run_root_dir"
    KEY_COPY_RESOURCES = "copy_resources"

    def _use_default(self):
        self.proj_path = self._proj_root_path
        self.run_root_dir = self._proj_root_path
        self.copy_res = None
        self.sub_url = None

    def _parse_info(self, cfg_info):
        super(WebConfig, self)._parse_info(cfg_info)
        if axmol.dict_contains(cfg_info, WebConfig.KEY_SUB_URL):
            self.sub_url = cfg_info[WebConfig.KEY_SUB_URL]
        else:
            self.sub_url = None

        if axmol.dict_contains(cfg_info, WebConfig.KEY_RUN_ROOT_DIR):
            self.run_root_dir = os.path.join(self._proj_root_path, cfg_info[WebConfig.KEY_RUN_ROOT_DIR])
        else:
            self.run_root_dir = None

        if axmol.dict_contains(cfg_info, WebConfig.KEY_COPY_RESOURCES):
            self.copy_res = cfg_info[WebConfig.KEY_COPY_RESOURCES]
        else:
            self.copy_res = None

    def _is_available(self):
        ret = super(WebConfig, self)._is_available()

        if ret:
            index_path = os.path.join(self.proj_path, "index.html")
            ret = os.path.isfile(index_path)

        return ret