'''
DESCRIPTION.
FreeCAD's tools for publishing .FCStd scene.
'''
import logging
import json
import os
import shutil
import FreeCAD
import freecad.robossembler.utils.freecad_tools as freecad_tools

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


def export_assembly_trees(doc, clones_dic=None) -> list:
    ''' Read FreeCAD .FCStd hierarchy and store it to assembly JSON config files. '''
    # determine root locators
    default_origins = []
    for obj in doc.Objects:
        if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
            if (hasattr(obj, 'Robossembler_DefaultOrigin')
                    and getattr(obj, 'Robossembler_DefaultOrigin')):
                default_origins.append(obj)
    if len(default_origins) > 1:
        root_locators = []
        for lcs in default_origins:
            if hasattr(lcs, 'Robossembler_RootLocator'):
                root_locators.append(getattr(lcs, 'Robossembler_DefaultOrigin'))
            else:
                logger.warning('RootLocator attribute not found for %s LCS! '
                               'So %s used as Root Locator!',
                               lcs.Label, lcs.InList[0].Label)
                root_locators.append(lcs.InList[0])
    else:
        root_locators = [
            root for root in doc.Objects
            if not root.InList
            if root.isDerivedFrom('App::Part')]

    trees = []
    for root_locator in root_locators:
        dict_tree = {}
        freecad_tools.hierarchy_tree(root_locator, dict_tree, clones_dic)
        trees.append(dict_tree)
    # write file
    project_dir = os.path.dirname(doc.FileName)
    trees_path = os.path.join(project_dir, 'trees.json')
    with open(trees_path, 'w', encoding='utf-8') as json_file:
        json.dump(trees, json_file, ensure_ascii=False, indent=4)
    logger.info('Assembly tree saved successfully to %s!', trees_path)

    logger.info('Saved %s assembly trees!', len(trees))

    return trees_path


def export_parts_database(
    doc,
        clones_dic=None, **tesselation_params):
    '''
    Collect parts database and export as JSON config file
    [
        {
            'name': '',
            'part_path': '',
            'material_path': ''
        },
        {...},
    ]
    '''
    # path directory
    project_dir = os.path.dirname(doc.FileName)
    materials_dir = os.path.join(project_dir, 'parts', 'materials')
    if os.path.exists(materials_dir):
        shutil.rmtree(materials_dir)
        os.makedirs(materials_dir)
    else:
        os.makedirs(materials_dir)
    objects_dir = os.path.join(project_dir, 'parts', 'objects')
    if os.path.exists(objects_dir):
        shutil.rmtree(objects_dir)
        os.makedirs(objects_dir)
    else:
        os.makedirs(objects_dir)

    # collect all materials
    fem_mats = [fem_mat for fem_mat in doc.Objects
                if fem_mat.isDerivedFrom('App::MaterialObjectPython')]
    material_paths = freecad_tools.get_material_paths()

    parts_db = []
    for obj in doc.Objects:

        # skip hidden objects
        if not obj.Visibility:
            continue

        # skip non part objects
        if not obj.isDerivedFrom('Part::Feature'):
            continue

        # skip lcs objects
        if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
            continue

        # skip non solid part objects
        forsed_nonsolid = (
            hasattr(obj, 'Robossembler_NonSolid')
            and getattr(obj, 'Robossembler_NonSolid'))
        if not freecad_tools.is_object_solid(obj) or forsed_nonsolid:
            logger.warning('Part has non solid shape! Please check %s', obj.Label)
            continue

        db_obj = {
            'name': '',
            'part_path': '',
            'material_path': ''
        }

        db_obj['name'] = obj.Label
        if clones_dic:
            for k, v in clones_dic.items():
                if obj.Label in v:
                    db_obj['name'] = k

        robossembler_attrs = [attr for attr in dir(obj) if 'Robossembler' in attr]
        if robossembler_attrs:
            db_obj['attributes'] = []
            for attr in robossembler_attrs:
                db_obj['attributes'].append({attr: getattr(obj, attr)})

        part_path = os.path.join(objects_dir, db_obj['name'] + '.stl')
        if os.path.exists(part_path):
            # this is clone
            continue
        mesh_from_shape = freecad_tools.tesselation(
            obj, doc,
            # TODO adaptive=(not forsed_nonsolid),
            adaptive=False,
            **tesselation_params
        )
        # export to stl files
        #Mesh.export([mesh_from_shape], part_path, tolerance=linear_deflection)
        mesh_from_shape.Mesh.write(part_path)
        db_obj['part_path'] = os.path.relpath(part_path, project_dir)
        logger.info('Part %s exported to stl file %s.', obj.Label, part_path)

        # find linked material path
        fem_mat_name = [fem_mat.Material['CardName']
                        for fem_mat in fem_mats
                        for ref in fem_mat.References
                        if ref[0].Label == obj.Label]
        if fem_mat_name:
            for source_path in material_paths:
                if fem_mat_name[0] not in source_path:
                    continue
                material_path = os.path.join(
                    materials_dir, os.path.basename(source_path))
                if not os.path.exists(material_path):
                    shutil.copy2(source_path, material_path)
                db_obj['material_path'] = os.path.relpath(material_path, project_dir)

        # append to database
        parts_db.append(db_obj)

    logger.info('Passed %s parts without errors', len(parts_db))
    parts_db_path = os.path.join(project_dir, 'parts.json')
    with open(parts_db_path, 'w', encoding='utf-8') as json_file:
        json.dump(parts_db, json_file, ensure_ascii=False, indent=4)
    logger.info('Parts Database exported successfully to %s!', parts_db_path)

    return parts_db_path

def save_FCStd_project(doc):
    if doc is not None:
        # path directory
        project_path = doc.FileName
        project_dir = os.path.dirname(project_path)
        if project_dir:
            project_dir, project_filename = os.path.split(project_path)
            new_path = os.path.join(project_dir, project_filename)
            FreeCAD.save(doc, new_path)
            FreeCAD.Console.PrintMessage("Проект сохранен в {path}\n".format(path=str(new_path)))
        else:
            FreeCAD.Console.PrintError("Проект не был сохранен, поэтому невозможно сохранить с тем же именем.\n")
    else:
        FreeCAD.Console.PrintError("Документ не найден.\n")
    


def publish_project_database(doc):
    '''
    Publish FCStd document to CG pipeline:
    - Exetute openned FreeCAD document.
    - Save FCStd document to database root path.
    - Export all assembly trees with LCS DefaultOrigin,
        or single one as JSON hierarchy tree.
    - Set tesselate solid parts to mesh.
    - Export all tesselated parts to STL files.
    - Return document as JSON dictionary.
    '''
    tesselation_params = {
        # Select tesselation method: Standard or FEM.
        'tesselation_method': 'Standard',
        # Max linear distance error
        'linear_deflection': 0.1,
        # Max angular distance error
        'angular_deflection': 20.0,
        # For FEM method only! Finite element size in mm
        'fem_size': 10.0
    }

    # TODO Save FCStd project file
    #doc.saveAs(u"/<file_dir>/<file_name>")

    clones_dic = freecad_tools.collect_clones(doc)
    tree_files = export_assembly_trees(doc, clones_dic)
    parts_data = export_parts_database(doc, clones_dic, **tesselation_params)

    try:
        doc.save()
    except OSError as e:
        FreeCAD.Console.PrintError("Document Not Saved!")
    else:
        FreeCAD.Console.PrintMessage('FreeCAD document {s} saved!\n'.format(s=doc.Label))

    # save_FCStd_project(doc)

    # logger.info('FreeCAD document %s published!', doc.Label)
    return doc.Label

def cmd_publish_project_database():
    doc = FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)
    publish_project_database(doc)