import os
import sys
import yaml
import string
import json
import inspect
from json import JSONEncoder, JSONDecoder
import re
import logging
import tempfile
import shutil
from collections import OrderedDict

yaml.add_constructor('!regexp', lambda l, n: re.compile(l.construct_scalar(n)))
SCHEMA_CONFIG_PATH = ".config/schema"

logger = logging.getLogger('fs_ctrl')
logger.propagate = True
logger.setLevel(logging.INFO)
if not logger.handlers:
    h = logging.StreamHandler()
    f = logging.Formatter("%(asctime)s %(levelname)s fs_ctrl: %(message)s", "%H:%M:%S")
    h.setFormatter(f)
    logger.addHandler(h)

def ordered_dict_constructor(loader, node):
    return OrderedDict(loader.construct_pairs(node))

class OrderedYamlLoader(yaml.Loader):
    pass

_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
OrderedYamlLoader.add_constructor(_mapping_tag, ordered_dict_constructor)


def type_by_name(name):
    members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
    for class_name, class_type in members:
        if name == class_name:
            return class_type

def _chgrp(filepath, group_name):
    import grp
    gid = grp.getgrnam(group_name)[2]
    uid = os.stat(filepath).st_uid
    os.chown(filepath, uid, gid)

class FSControlError(Exception):
    pass


class FsNode(object):
    def __init__(self, project_schema, name):
        super(FsNode, self).__init__()
        self.project_schema = project_schema
        self.name = name
        self.parent = None
        self.children = []

        self._fullpath_cached = None

    def get_type(self):
        return __FOLDER_CLASS_MAPPING_REVERSED__.get(self.__class__)

    def get_all_parents(self):
        result = []
        current_parent = self.parent
        while current_parent is not None:
            result.append(current_parent)
            current_parent = current_parent.parent
        result.reverse()
        return result

    def is_valid_name(self, name):
        """
        Is specified name are ok for this fs_node
        """
        return self.name == name

    def get_path_to_parent(self, parent_node):
        path_parts = []
        current_parent = self
        while current_parent is not None:
            if current_parent == parent_node:
                break
            path_parts.insert(0, current_parent.name)
            current_parent = current_parent.parent
        path = "/".join(path_parts)
        return path

    def get_fullpath(self):
        if self._fullpath_cached is not None:
            return self._fullpath_cached
        path_parts = []
        path_parts.insert(0, self.name)
        current_parent = self.parent
        while current_parent != None:
            path_parts.insert(0, current_parent.name)
            current_parent = current_parent.parent
        full_path = "/".join(path_parts)
        self._fullpath_cached = full_path
        return full_path

    # def find_child(self, child_name):
    #     for child in self.children:
    #         if child.name == child_name:
    #             return child

    def find_child_partial(self, child_name):
       for child in self.children:
            if child.is_valid_name(child_name) :
                return child

    def find(self, search_path):
        stack = [(self.get_fullpath(), self)]
        while stack:
            node_path, node = stack.pop()
            if node_path == search_path:
                return node

            for child in node.children:
                if child.get_fullpath() in search_path:
                    stack.append( (child.get_fullpath(), child)  )

    def iter_hier(self, resolve_dict = None):
        stack = [ self]
        while stack:
            node = stack.pop()
            def stop_cond(n):
                if resolve_dict is None or n.get_type() is None:
                    return False
                return False if n.get_type() in resolve_dict else True
            if stop_cond(node) ==  False:
                yield node
            for child in node.children:
                if stop_cond(child) ==  False:
                    stack.append( child  )
        return

    def __repr__(self):
        return "%s(fullpath='%s', name='%s')" % (self.__class__.__name__, self.get_fullpath(), self.name)

    def to_json(self):
        attr_dict = {}
        for attr in ['name', '_fullpath_cached']:
            attr_dict[attr] = self.__dict__.get(attr)

        if self.children:
            attr_dict['children'] = []
            for child in self.children:
                attr_dict['children'].append(child.to_json())

        attr_dict['py_class_type'] = self.__class__.__name__

        return attr_dict

    @staticmethod
    def from_json(attr_dict, schema):
        class_type = type_by_name(attr_dict.get('py_class_type'))
        obj = class_type(schema, attr_dict.get('name'))

        for attr in ['_fullpath_cached']:
            obj.__dict__[attr] = attr_dict.get(attr)

        obj.set_children_json(attr_dict.get('children'), obj, schema)

        return obj

    @staticmethod
    def set_children_json(children_dict, obj, schema):
        if children_dict:
            for child in children_dict:
                child = type_by_name(child.get('py_class_type')).from_json(child, schema)
                child.parent = obj
                obj.children.append(child)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        for attr_name, attr_val in self.__dict__.items():
            # Don't check project_schema and parents because of recursion
            # Don't check _components and component_parent_node also because of recursion but moreover
            # because this components was not used anywhere
            if attr_name not in ('project_schema', 'parent', '_components', 'component_parent_node') \
                    and other.__dict__.get(attr_name) != attr_val:
                return False
        return True

    def __ne__(self, other):
        return not self == other


class SymlinkFile(FsNode):
    def __init__(self, project_schema, name, src_path):
        super(SymlinkFile, self).__init__(project_schema, name)
        self.src_path = src_path

    def to_json(self):
        attr_dict = FsNode.to_json(self)
        for attr in ['src_path']:
            attr_dict[attr] = self.__dict__.get(attr)

        return attr_dict

    @staticmethod
    def from_json(attr_dict, schema):
        obj = SymlinkFile(schema, attr_dict.get('name'), attr_dict.get('src_path'))
        obj.set_children_json(attr_dict.get('children'), obj, schema)

        for attr in ['_fullpath_cached']:
            obj.__dict__[attr] = attr_dict.get(attr)
        return obj


class VCSSchemaFile(FsNode):
    def __init__(self, project_schema, name, src_path):
        super(VCSSchemaFile, self).__init__(project_schema, name)
        self.src_path = src_path
        self.vcs_schema = {}

    def get_vcs_schema(self):
        return self.vcs_schema

    def load_vcs_schema(self):
        data = None
        path = self.src_path
        with open(path, 'r') as f:
            doc = yaml.Loader(f)
            try:
                if not doc.check_data():
                    raise FSControlError("Cannot load yaml file %s" % path )
                data = doc.get_data()
            finally:
                doc.dispose()

        self.vcs_schema = data

    def to_json(self):
        attr_dict = FsNode.to_json(self)
        for attr in ['src_path', 'vcs_schema']:
            attr_dict[attr] = self.__dict__.get(attr)

        return attr_dict

    @staticmethod
    def from_json(attr_dict, schema):
        obj = VCSSchemaFile(schema, attr_dict.get('name'), attr_dict.get('src_path'))

        for attr in ['_fullpath_cached', 'vcs_schema']:
            obj.__dict__[attr] = attr_dict.get(attr)

        obj.set_children_json(attr_dict.get('children'), obj, schema)
        return obj


class Folder(FsNode):
    def __init__(self, project_schema, name = "$root"):
        super(Folder, self).__init__(project_schema, name)
        self.schema_attrs = {}
        self.is_component = False
        self.component_name = None
        self.component_parent_node = None

    def need_to_create_template(self):
        return self.schema_attrs.get('create_template', False)
    
    def is_vcs(self):
        return self.schema_attrs.get('vcs', False)

    def set_component(self, component_name, component_parent_node):
        self.is_component = True
        self.component_name = component_name
        self.component_parent_node = component_parent_node
    
    def get_doc(self):
        return self.schema_attrs.get("doc")

    def is_valid_name(self, name):
        """
        Is specified folder name are ok for this folder type
        """
        if self.schema_attrs.get("validator"):
            match = self.schema_attrs.get("validator").match(name)
            if match:
                return True
            else:
                return False
        return self.name == name

    def to_json(self):
        attr_dict = FsNode.to_json(self)
        for attr in ['is_component', 'component_name']:
            if self.__dict__.get(attr) is not None:
                attr_dict[attr] = self.__dict__.get(attr)

        # don't serialize components for now
        # if self.__dict__.get('_components'):
        #      cmpn_dict = {}
        #      for key, value in self._components.items():
        #          if isinstance(value, FsNode):
        #              cmpn_dict[key] = value.to_json()
        #          else:
        #              cmpn_dict[key] = value
        #      attr_dict['_components'] = cmpn_dict

        if self.schema_attrs:
            attr_dict['schema_attrs'] = {}
            for key, value in self.schema_attrs.items():
                if type(value).__name__ in ('SRE_Pattern', 'Pattern'):
                    attr_dict['schema_attrs'][key] = {'type': 'SRE_Pattern', 'pattern': value.pattern}
                else:
                    attr_dict['schema_attrs'][key] = value

        return attr_dict

    @staticmethod
    def from_json(attr_dict, schema):
        class_type = type_by_name(attr_dict.get('py_class_type'))
        obj = class_type(schema, attr_dict.get('name'))

        for attr in ['is_component', 'component_name', '_fullpath_cached']:
            obj.__dict__[attr] = attr_dict.get(attr)

        if attr_dict.get('schema_attrs'):
            obj.schema_attrs = {}
            for key, value in attr_dict.get('schema_attrs').items():
                if type(value) == dict and value.get('type') =='SRE_Pattern':
                    obj.schema_attrs[key] = re.compile(value.get('pattern'))
                else:
                    obj.schema_attrs[key] = value

        obj.set_children_json(attr_dict.get('children'), obj, schema)
        return obj


class TemplateFolder(Folder):
    def __init__(self,*args,**kwargs):
        super(TemplateFolder, self).__init__(*args,**kwargs)
        self._components = {}

    @property
    def is_component_parent(self):
        return len(self._components) > 0

    def get_component_list(self):
        return list(self._components.keys())

    def find_component_by_name(self, name):
        return self._components.get(name)

    def add_component(self, name, fs_node):
        self._components[name] = fs_node

    def get_key(self):
        return self.name.replace("$", '')


class SeriesTemplateFolder(TemplateFolder):
    def __init__(self,*args,**kwargs):
        super(SeriesTemplateFolder, self).__init__(*args,**kwargs)


class EpisodeTemplateFolder(TemplateFolder):
    def __init__(self,*args,**kwargs):
        super(EpisodeTemplateFolder, self).__init__(*args,**kwargs)


class SceneTemplateFolder(TemplateFolder):
    def __init__(self,*args,**kwargs):
        super(SceneTemplateFolder, self).__init__(*args,**kwargs)


class AssetTemplateFolder(TemplateFolder):
    def __init__(self,*args, **kwargs):
        super(AssetTemplateFolder, self).__init__(*args,**kwargs)


class VariantTemplateFolder(TemplateFolder):
    def __init__(self,*args, **kwargs):
        super(VariantTemplateFolder, self).__init__(*args,**kwargs)


__FOLDER_CLASS_MAPPING__  = {"scene": SceneTemplateFolder, "episode": EpisodeTemplateFolder, "asset": AssetTemplateFolder, "series": SeriesTemplateFolder, "variant": VariantTemplateFolder, None: Folder}
__FOLDER_CLASS_MAPPING_REVERSED__ = dict(zip(__FOLDER_CLASS_MAPPING__.values(), __FOLDER_CLASS_MAPPING__.keys()))


class ProjectFileSchema(object):
    def __init__(self, proj_name=None, project_path=None):
        if not proj_name or not project_path:
            # for the case when we need to rebuild schema from json
            return
        self.project_path = project_path

        self.root_node = Folder(self)

        self._load_schema()
        self._proj_name = proj_name
        self._vcs_admin_cfg_path = None
        self._components = {}

    @property
    def proj_name(self):
        return self._proj_name

    def get_components(self):
        return self._components

    def get_components_by_context(self, path):
        result = {}
        for comp_key, comp_data in self.get_components().items():
            if comp_data.get("context_path") == path:
                result[comp_key] = comp_data
        return result

    def load_components(self):
        self._components = self.load_components_config()
        for comp_key, comp_data in self._components.items():
            pub_path = comp_data.get("publish_path")
            if not pub_path:
                raise FSControlError("Cannot find 'publish_path' key for component '%s'" % comp_key)
            ctx_path = comp_data.get("context_path")
            if not ctx_path:
                raise FSControlError("Cannot find 'context_path' key for component '%s'" % comp_key)
            pub_node = self.root_node.find(pub_path)
            if not pub_node:
                raise FSControlError("Cannot find 'publish_path' node '%s' key for component '%s'" % (pub_path, comp_key))
            ctx_node = self.root_node.find(ctx_path)
            if not ctx_node:
                raise FSControlError("Cannot find 'context_path' node '%s' key for component '%s'" % (ctx_path, comp_key))
            if not isinstance(ctx_node, TemplateFolder):
                raise FSControlError("'context_path' node '%s' key for component '%s' is not of 'TemplateFolder' type" % (ctx_path, comp_key))

            pub_node.set_component(comp_key, ctx_node)
            ctx_node.add_component(comp_key, pub_node)

    def check_is_valid(self):
        path = os.path.join(self.project_path, '.config/fs-ctrl.yml')
        try:
            fs_control_cfg = self.load_fs_ctrl_config()
        except:
            raise FSControlError("Error loading fs_ctrl config '%s'" % path)

        for fs_node in self.root_node.iter_hier():
            if isinstance(fs_node, VCSSchemaFile):
                data = None
                try:
                    data = fs_node.get_vcs_schema()
                except ValueError as e:
                    raise FSControlError("Error parsing VCSchema '%s': %s" % (fs_node.src_path, e) )

                if data.get('type') is None:
                    raise FSControlError("VCSchema not found required field 'type' at '%s' " % fs_node.src_path )

                if data.get('publish_permission') is None:
                    raise FSControlError("VCSchema not found required field 'publish_permission' at '%s' " % fs_node.src_path )

    def load_fs_ctrl_config(self):
        path = os.path.join(self.project_path, '.config/fs-ctrl.yml')
        with open(path, 'r') as f:
            doc = yaml.Loader(f)
            try:
                if not doc.check_data():
                    raise FSControlError("Cannot load yaml file %s" % path )
                data = doc.get_data()
            finally:
                doc.dispose()

        return data

    def load_components_config(self):
        path = os.path.join(self.project_path, '.config/components.yml')
        with open(path, 'r') as f:
            doc = yaml.Loader(f)
            try:
                if not doc.check_data():
                    raise FSControlError("Cannot load yaml file %s" % path )
                data = doc.get_data()
            finally:
                doc.dispose()

        return data

    def set_vcs_config_path(self, path):
        self._vcs_admin_cfg_path = path

    def _get_dirs(self, parent_path):
        return [os.path.join(parent_path, path) for path in os.listdir(parent_path) if os.path.isdir(parent_path)]

    def _load_schema_file(self, path):
        with open(path, 'r') as f:
            doc = OrderedYamlLoader(f)
            try:
                if not doc.check_data():
                    raise FSControlError("Cannot load yaml file %s" % path )
                data = doc.get_data()
            finally:
                doc.dispose()

        return data

    def _get_files(self, parent_path):
        result = []
        for file_name in os.listdir(parent_path):
            full_path = os.path.join(parent_path, file_name)

            if not os.path.isfile(full_path):
                continue
            if file_name.endswith(".yml"):
                continue

            if file_name.endswith(".symlink"):
                continue

            result.append(full_path)
        return result

    def _load_schema(self):
        self._process_config(self.root_node, os.path.join(self.project_path, SCHEMA_CONFIG_PATH))

    def _process_config(self, parent_node, parent_path):
        for entry in os.listdir(parent_path):
            full_path = os.path.join(parent_path, entry)
            if entry == "schema.yml":
                schema_data = self._load_schema_file(full_path)
                #parent_node.schema = schema_data
                for key, value in schema_data.items():
                    schema_folder_path = os.path.join(parent_path, key)
                    if os.path.exists(schema_folder_path):
                        child_node = None
                        child_node = __FOLDER_CLASS_MAPPING__[value.get('type')](project_schema = self, name = key)
                        child_node.parent = parent_node
                        child_node.schema_attrs = value
                        parent_node.children.append(child_node)
                        #print child_node.get_doc()
                        self._process_config(child_node, schema_folder_path)
            elif entry.endswith(".symlink"):
                child_node = SymlinkFile(self, entry.split(".symlink")[0], full_path )
                child_node.parent = parent_node
                parent_node.children.append(child_node)
            elif entry == "vcs_schema.yml":
                child_node = VCSSchemaFile( self, entry, full_path )
                child_node.load_vcs_schema()
                child_node.parent = parent_node
                parent_node.children.append(child_node)

    def _path_exists(self, fs_node, resolve_dict):
        if isinstance(fs_node, FsNode):
            return os.path.exists( self._resolve_node_path(fs_node.get_fullpath(), resolve_dict) )

    def _folder_create(self, fs_node, resolve_dict):
        if isinstance(fs_node, Folder):
            folder_path = self._resolve_node_path(fs_node.get_fullpath(), resolve_dict)
            if not os.path.exists(folder_path):
                os.mkdir(folder_path)
                logger.info("created folder '%s'", folder_path)
            else:
                logger.info("path '%s' exist, skipping ", folder_path)
            write_permissions = fs_node.schema_attrs.get("write_permissions")
            if write_permissions:
                #Should be facl but we have techical difficulties for now
                if len(write_permissions) > 1:
                    logger.warning("set write permission for folder '%s' muliple write permissions not supported " % folder_path )
                for group_name in write_permissions:
                    _chgrp(folder_path, group_name)
                    os.chmod(folder_path, int('775', 8) )
                    logger.info("set write permission for folder '%s' for group '%s' " % (folder_path, group_name) )
                    break
        else:
            raise FSControlError('passed object is not folder')

    def _get_components_item_name(self, publish_path):
        for item, data in self._components.items():
            data_publish_path = data.get('publish_path')
            if data_publish_path is not None and data_publish_path == publish_path:
                return item

    def _get_item_template_path(self, item_name):
        return '{}/.config/templates/{}'.format(self.project_path, item_name)

    def _write_temp_file(self, path, data):
        try:
            with open(path, 'w') as temp_file:
                temp_file.write(data)
        except IOError as e:
            raise FSControlError(str(e))

    def _create_template_files(self, templates_path, temp_folder, resolve_dict):
        render_env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_path))
        template_list = os.listdir(templates_path)

        for template in template_list:
            output_text = render_env.get_template(template).render(resolve_dict)
            file_name = self._resolve_node_path(template, resolve_dict)
            self._write_temp_file(os.path.join(temp_folder, file_name), output_text)

    def _get_version_files(self, temp_folder, template_files):
        version_files = {}
        for template_file in template_files:
            try:
                version_files[template_file] = open(os.path.join(temp_folder, template_file), 'rb')
            except IOError as e:
                raise FSControlError(str(e))
        return version_files

    def load_assetvcs_info(self, path):
        data = {}
        try:
            with open(path) as assetvcs_info:
                data = json.loads(assetvcs_info.read())
        except IOError as e:
            raise FSControlError("Can`t read assetvcs_info file: {} - {}".format(path, str(e)))
        return data

    def _make_vcs_folder(self, fs_node, resolve_dict, debug=False):
        from wizart.fs_ctrl.assetvcs import AssetVcsAPI
        assetvcs_api = AssetVcsAPI()

        if fs_node.is_vcs():
            folder_path = self._resolve_node_path(fs_node.get_fullpath(), resolve_dict)
            schema_node = None
            for child in fs_node.children:
                if isinstance(child, VCSSchemaFile):
                    schema_node = child
            if schema_node == None:
                raise FSControlError("Cannot find VCSSchemaFile for '%s'" % folder_path)

            if not os.path.exists(folder_path):
                os.mkdir(folder_path)
                logger.info("created folder '%s'", folder_path)
            else:
                logger.info("path '%s' exist, skipping ", folder_path)

            if debug:
                return

            asset_id = None
            if os.path.exists(os.path.join(folder_path, 'assetvcs-info')):
                logger.info(
                    "path '%s' already have 'assetvcs-info', skip assetvcs-admin step " % folder_path)
                asset_data = self.load_assetvcs_info(
                    os.path.join(folder_path, 'assetvcs-info'))
                asset_id = asset_data["_id"]
            else:
                schema_data = schema_node.get_vcs_schema()
                permission_cmds = []
                for permission in schema_data['publish_permission']:
                    permission_cmds.append(permission)

                data = {
                    "project": self.proj_name,
                    "asset_type": schema_data['type'],
                    "asset_location": folder_path,
                    "permission": permission_cmds
                }

                logger.info("Call assetvcs: '%s'", data)
                asset_data = assetvcs_api.create_asset(data)

                asset_id = asset_data['_id']

            asset_versions = assetvcs_api.get_asset_versions(asset_id)

            if asset_versions:
                logger.info('Version already exists')
                return

            if not fs_node.need_to_create_template():
                return

            self.load_components()

            item_name = self._get_components_item_name(fs_node.get_fullpath())
            if item_name is not None:
                templates_path = self._get_item_template_path(item_name)

                if os.path.exists(templates_path):
                    logger.info("Creating a new version")
                    temp_folder = tempfile.mkdtemp()
                    self._create_template_files(
                        templates_path, temp_folder, resolve_dict)

                    template_files = os.listdir(temp_folder)
                    if not template_files:
                        raise FSControlError(
                            "Template files not found - {}".format(temp_folder))

                    version_data = assetvcs_api.create_version(asset_id)
                    version_num = version_data['version']

                    version_files = self._get_version_files(
                        temp_folder, template_files)
                    assetvcs_api.upload_version_files(
                        asset_id, version_num, version_files)

                    publish_data = {
                        "file": template_files
                    }
                    assetvcs_api.publish_version(
                        asset_id, version_num, publish_data)
                    logger.info(
                        "Successfully created a new version - asset_id: {}, version_num: {}".format(asset_id, version_num))

                    try:
                        shutil.rmtree(temp_folder)
                    except (IOError, OSError) as e:
                        logger.error(str(e))
        else:
            raise FSControlError('passed object is not vcs enabled folder')

    def _make_symlink(self, fs_node, resolve_dict):
        if isinstance(fs_node, SymlinkFile):
            symlink_path = self._resolve_node_path(fs_node.get_fullpath(), resolve_dict)
            if not os.path.exists(symlink_path):
                os.symlink(fs_node.src_path, symlink_path)
                logger.info("created symlink '%s' from '%s'", symlink_path, fs_node.src_path)
            else:
                logger.info("path '%s' exist, skipping ", symlink_path)
        else:
            raise FSControlError('passed object is not symlink')

    def _resolve_node_path(self, path, resolve_dict):
        return str(  string.Template(path).substitute(resolve_dict ) )
    
    def find_node_by_wref(self, wref_uri, return_resolved_vars = False):
        resolved_dict = {}
        from urlparse import urlparse, urlunparse, parse_qs
        parse_res = urlparse(wref_uri) 

        wref_path = "{scheme}://{netloc}{path}".format(**parse_res._asdict())
        wref_project_path = self.project_path.replace("W:", "wref:/")

        fs_search_path = wref_path.split("%s/" % wref_project_path, 1)[1]
        path_components = fs_search_path.split('/')
        query = parse_qs(parse_res.query)

        if 'path' in query:
            resolved_dict["path"] = query["path"][0]
        path_components.reverse()
        root_node = self.root_node
        while root_node is not None and len(path_components) > 0:
            path_component = path_components.pop()
            found_node = root_node.find_child_partial(path_component)
            if found_node and isinstance(found_node, TemplateFolder):
                resolved_dict[found_node.get_key()] = path_component
            if found_node and len(path_components) == 0:
                return (found_node, resolved_dict) if return_resolved_vars else found_node
            root_node = found_node
        #found_node = self.root_node.find(fs_search_path )
        return  (found_node, {}) if return_resolved_vars else found_node

    def get_wref_path_to_file(self, component_name, get_dir_path=False, file_name=None, **kwargs):
        """
        :param component_name: str defining component.
        :param get_dir_path: bool flag. If True return path to component folder without ?path=...
        :param file_name: unresolved file_name. If in component many files, user can choose which path he want.
                            If this param not defined, we use file marked as required in schema
        :param **kwargs: arguments to resolve path
        :return: tuple: wref_path to file and file name
        """
        component = self._components.get(component_name)
        if not component:
            return

        publish_path = component['publish_path']
        resolve_dict = dict(root=self.project_path.replace('W:', 'wref:/'))
        resolve_dict.update(kwargs)

        resolved_publish_path = self._resolve_node_path(publish_path, resolve_dict)

        # if user want path to component folder
        if get_dir_path:
            return resolved_publish_path, ''

        # if there aren't file_name provided by user, we use file marked as required in schema
        if not file_name:
            component_node = self.root_node.find(publish_path)
            if not component_node or not component_node.children:
                return

            file_node = component_node.children[0]
            vcs_schema = file_node.vcs_schema
            version_content = vcs_schema.get('version_content')
            file_name = ''
            for vers_path in version_content:
                if vers_path.get('required', False):
                    file_name = vers_path.get('path')
                    break
        resolved_file_name = file_name.format(**resolve_dict)

        return '{component_path}?path={file_name}'.format(component_path=resolved_publish_path,
                                                          file_name=resolved_file_name), resolved_file_name

    def find_node_by_path(self, path, return_resolved_vars = False, check_exists = True):
        resolved_dict = {}

        early_out = lambda: (None, resolved_dict) if return_resolved_vars else None
        if not os.path.exists(path) and check_exists is True:
            return early_out()

        if os.path.realpath(path) == os.path.realpath(self.project_path):
            resolved_dict = {'root': self.project_path}
            return self.root_node, resolved_dict if return_resolved_vars else None

        start_path = os.path.relpath(path, self.project_path)

        def _update_path_key():
            if "path" in resolved_dict:
                resolved_dict['path'] = "/".join(resolved_dict['path'])

        #windows handling
        start_path = start_path.replace('\\','/')
        path_components = start_path.split('/')
        path_components.reverse()
        root_node = self.root_node

        version_folder_expr = re.compile("^(v[0-9]{3}|[a-z]+)$")
        latest_found_node = None
        while len(path_components) > 0:
            path_component = path_components.pop()
            found_node = None
            if root_node:
                found_node = root_node.find_child_partial(path_component)
                if found_node:
                    latest_found_node = found_node
            if found_node and isinstance(found_node, TemplateFolder):
                resolved_dict[found_node.get_key()] = path_component
            elif not found_node and re.match(version_folder_expr, path_component):
                resolved_dict["version"] = path_component
            elif not found_node:
                resolved_dict.setdefault("path",[]).append(path_component)
            if found_node and len(path_components) == 0:
                _update_path_key()
                return (found_node, resolved_dict) if return_resolved_vars else found_node
            elif len(path_components) == 0:
                _update_path_key()
                return (latest_found_node, resolved_dict)  if return_resolved_vars else latest_found_node 
            root_node = found_node
        return early_out()

    def symlink_templated_folder(self, template_path, src_root, dst_root, **kwargs):
        """
        Main usage is freelance ftp server.
        We want to upload all assets to common resource folder,
        But symlink for each artist allowed assets
        """
        template_folder = self.root_node.find(template_path)
        if template_folder is None:
            raise FSControlError("Not found template_path")
        if "root" in kwargs:
            kwargs.pop("root")

        if os.path.exists(src_root) is False:
            raise FSControlError("src_root '%s' not exists" % src_root)

        if os.path.exists(dst_root) is False:
            raise FSControlError("src_root '%s' not exists" % src_root)

        if not os.path.isabs(dst_root):
            dst_root = os.path.abspath(dst_root)
        if not os.path.isabs(src_root):
            src_root = os.path.abspath(src_root)

        dst_resolve_dict = dict(root = dst_root)
        dst_resolve_dict.update(kwargs)

        src_resolve_dict = dict(root = src_root)
        src_resolve_dict.update(kwargs)

        src_folder_path = self._resolve_node_path(template_folder.get_fullpath(), src_resolve_dict)
        dst_folder_path = self._resolve_node_path(template_folder.get_fullpath(), dst_resolve_dict)

        if os.path.exists(dst_folder_path):
            logger.info("path '%s' exist, skipping ", dst_folder_path)
            return
        if not os.path.exists(src_folder_path):
            raise FSControlError("Not found symlink source folder '%s'" % src_folder_path)

        def _pass(create = False):
            for folder in template_folder.get_all_parents():
                    self.__schema_name_validate(folder, dst_resolve_dict)
                    if create:
                        self._folder_create(folder, dst_resolve_dict)

            #create our templated folder
            self.__schema_name_validate(template_folder, dst_resolve_dict)
            if create:
                if not os.path.exists(dst_folder_path):
                    os.symlink(src_folder_path, dst_folder_path)
                    logger.info("created symlink '%s' from '%s'", dst_folder_path, src_folder_path)
                else:
                    logger.info("path '%s' exist, skipping ", dst_folder_path)

        #prepass check if everything is valid
        _pass(create = False)
        #real pass creation
        _pass(create = True)

    def __schema_name_validate(self, folder, resolve_dict):
        folder_type = folder.get_type()
        if folder_type in resolve_dict and folder.schema_attrs.get("validator"):
            match = folder.schema_attrs.get("validator").match(resolve_dict[folder_type])
            if match is None:
                raise FSControlError("invalid name, regex mismatch '%s' for type '%s' " % (resolve_dict[folder_type], folder_type) )

    def create_templated_folder(self, template_path, debug=False, **kwargs):
        template_folder = self.root_node.find(template_path)
        if template_folder is None:
            raise FSControlError("Not found template_path")
        if "root" in kwargs:
            kwargs.pop("root")

        resolve_dict = dict(root = self.project_path)
        resolve_dict.update(kwargs)

        def _pass(create = False):
            #create parents
            for folder in template_folder.get_all_parents():
                    self.__schema_name_validate(folder, resolve_dict)
                    if create:
                        self._folder_create(folder, resolve_dict)

            #create our templated folder
            self.__schema_name_validate(template_folder, resolve_dict)
            if create:
                self._folder_create(template_folder, resolve_dict)

            #create children
            for folder in template_folder.iter_hier(resolve_dict):
                if isinstance(folder, Folder):
                    self.__schema_name_validate(folder, resolve_dict)
                if create:
                    if isinstance(folder, SymlinkFile):
                        self._make_symlink(folder, resolve_dict)
                    elif isinstance(folder, VCSSchemaFile):
                        continue
                    elif folder.is_vcs():
                        self._make_vcs_folder(folder, resolve_dict, debug=debug)
                    else:
                        self._folder_create(folder, resolve_dict)
        #prepass check if everything is valid
        _pass(create = False)
        #real pass creation
        _pass(create = True)

    @staticmethod
    def from_json(attr_dict):
        schema = ProjectFileSchema()
        for attr in ['project_path', '_proj_name', '_vcs_admin_cfg_path', '_components']:
            schema.__dict__[attr] = attr_dict.get(attr)

        class_type = type_by_name(attr_dict.get('hierarchy').get('py_class_type'))
        obj = class_type(schema, attr_dict.get('name'))
        schema.root_node = obj.from_json(attr_dict['hierarchy'], schema)
        return schema

    @staticmethod
    def from_json_file(file):
        with open(file, 'r') as f:
            schema = json.load(f, cls=ProjectFileSchemaDecoder)

        return schema

    def __eq__(self, other):
        if not isinstance(other, ProjectFileSchema):
            return NotImplemented
        for attr_name, attr_val in self.__dict__.items():
            if other.__dict__.get(attr_name) != attr_val:
                return False
        return True

    def to_json(self):
        res_dict = ProjectFileSchemaEncoder().default(self)
        return res_dict

    def list_dir(self, path):
        variables = {'root': self.project_path}
        path = str(string.Template(path).safe_substitute(variables)).split('$')[0]
        node = self.find_node_by_path(path)

        res_names = []
        if node:
            for name in os.listdir(path):
                child_path = os.path.join(path, name)
                child_node = self.find_node_by_path(child_path)
                if child_node:
                    res_names.append(name)

        return res_names


class ProjectFileSchemaEncoder(JSONEncoder):
    def default(self, obj):
        def parse_child(node):
            node_data = node.__dict__
            child_data = []
            for child in node.children:
                child_data.append(parse_child(child))

            node_data['chilren'] = child_data
            return node_data

        if isinstance(obj, ProjectFileSchema):
            res_dict = dict()

            res_dict['_type'] = 'ProjectFileSchema'
            res_dict['project_path'] = obj.project_path
            res_dict['_proj_name'] = obj._proj_name
            res_dict['_vcs_admin_cfg_path'] = obj._vcs_admin_cfg_path
            res_dict['hierarchy'] = obj.root_node.to_json()
            res_dict['_components'] = obj._components
            return res_dict

        else:
            return json.JSONEncoder.default(self, obj)


class ProjectFileSchemaDecoder(JSONDecoder):
    def decode(self, str, *args, **kwargs):
        obj = JSONDecoder.decode(self, str, *args, **kwargs)
        if '_type' not in obj:
            return obj

        type = obj['_type']
        if type == 'ProjectFileSchema':
            new_obj = ProjectFileSchema.from_json(obj)
            return new_obj
        else:
            return obj


def test_json_encoder_decoder():
    #os.environ['PROJECT_NAME'] = 'Snow Queen Series'
    #os.environ['PROJECT_ROOT_PATH'] = 'W:/projects/sqs'

    os.environ['PROJECT_NAME'] = 'HG'
    os.environ['PROJECT_ROOT_PATH'] = 'W:/projects/hg'
    fs_sch = ProjectFileSchema(os.environ["PROJECT_NAME"], os.environ["PROJECT_ROOT_PATH"])
    fs_sch.load_components()
    jsonString = ProjectFileSchemaEncoder().encode(fs_sch)
    fs_sch_recovered = json.loads(jsonString, cls=ProjectFileSchemaDecoder)

    if fs_sch == fs_sch_recovered:
        return True
    else:
        return False



