import FreeCAD as App
import uuid
import os
import json
from typing import List, Dict, Any, TypeVar, Callable, Type, cast
from itertools import repeat


class CoreList(List):
    # the list contains only True
    def onlyTrue(self) -> bool:
        print(self)
        for el in self:
            if el is not True:
                return False
        return True


def isInListRange(listIn, index):
    try:
        listIn[index]
        return False
    except:
        return True


class AllSequences:
    all_sequences = None
    adj_matrix = None
    topologyIds = None
    adj_matrix_names = None

    def __init__(self, adj_matrix) -> None:
        self.adj_matrix = adj_matrix
        self.all_possible_sequences(self.adj_matrix)
        self.matrix_by_name()
        pass

    def matrix_by_name(self):
        result = self.all_sequences
        inc = 0
        for matrix in self.all_sequences:
            for index in range(len(matrix)):
                result[inc][index] = list(
                    filter(
                        lambda el: el.get("number") == matrix[index] + 1,
                        self.topologyIds,
                    )
                )[0].get("name")
            inc += 1
        self.adj_matrix_names = result
        pass

    def find_all_sequences(self, adj_matrix):
        sequences = []
        num_vertices = len(adj_matrix)

        def dfs(vertex, sequence):
            sequence.append(vertex)

            if len(sequence) == num_vertices:
                sequences.append(sequence)
                return

            for i in range(num_vertices):
                if adj_matrix[vertex][i] == 1 and i not in sequence:
                    dfs(i, sequence.copy())

        for i in range(num_vertices):
            dfs(i, [])

        self.all_sequences = sequences

    def findId(self, listMatrix, id):
        def filter_odd_num(in_num):
            if in_num["name"] == id:
                return True
            else:
                return False

        return list(filter(filter_odd_num, listMatrix))[0]["number"]

    def iter_paths(self, adj, min_length=6, path=None):
        if not path:
            for start_node in range(len(adj)):
                yield from self.iter_paths(adj, min_length, [start_node])
        else:
            if len(path) >= min_length:
                yield path
            if path[-1] in path[:-1]:
                return
            current_node = path[-1]
            for next_node in range(len(adj[current_node])):
                if adj[current_node][next_node] == 1:
                    yield from self.iter_paths(adj, min_length, path + [next_node])

    def allUnique(self, x):
        seen = set()
        return not any(i in seen or seen.add(i) for i in x)

    def all_possible_sequences(self, matrix):
        topologyIds = []
        topologyMatrixNumber = {}
        inc = 0
        for k, v in matrix.items():
            inc += 1
            topologyIds.append({"name": k, "number": inc})
        inc = 0
        for k, v in matrix.items():
            inc += 1
            topologyMatrixNumber[inc] = list(
                map(lambda el: self.findId(topologyIds, el), v)
            )
        self.topologyIds = topologyIds
        adj = []
        matrixSize = matrix.keys().__len__()
        inc = 0
        for k, v in topologyMatrixNumber.items():
            adj.append(list(repeat(0, matrixSize)))
            for el in v:
                adj[inc][el - 1] = 1
            inc += 1
        self.find_all_sequences(adj)


class VectorModel:
    x: float
    y: float
    z: float

    def __init__(self, cadVector) -> None:
        self.x = cadVector[0]
        self.y = cadVector[1]
        self.z = cadVector[2]
        pass

    def toFreeCadVector(self):
        return App.Vector(self.x, self.y, self.z)


class FreeCadRepository:
    _solids = []

    def isAllObjectsSolids(self) -> List[str]:
        result = []
        for part in App.ActiveDocument.Objects:
            if self.is_object_solid(part) is False:
                result.append(part.Label)
        return result

    def objectSetPosition(self, solid, cadVector):
        solid.Placement.Base = cadVector
        pass

    def objectGetPosition(self, solid) -> VectorModel:
        return VectorModel(cadVector=solid.Placement.Base)

    def isObjectIntersections(self, part) -> bool:
        for solid in self.getAllSolids():
            if solid.Label != part.Label:
                collisionResult: int = int(part.Shape.distToShape(solid.Shape)[0])
                if collisionResult == 0:
                    return True
        return False

    def objectHasTouches(self, part, objectIndentation: float) -> List[str]:
        positionVector = self.objectGetPosition(part)
        result = CoreList()
        result.append(self.isObjectIntersections(part=part))

        if objectIndentation != 0 and objectIndentation != None:
            result.append(
                self.axis_movement_and_intersections_observer(
                    positionVector=positionVector,
                    alongAxis="x",
                    objectIndentation=objectIndentation,
                    part=part,
                )
            )
            result.append(
                self.axis_movement_and_intersections_observer(
                    positionVector=positionVector,
                    alongAxis="y",
                    objectIndentation=objectIndentation,
                    part=part,
                )
            )
            result.append(
                self.axis_movement_and_intersections_observer(
                    positionVector=positionVector,
                    alongAxis="z",
                    objectIndentation=objectIndentation,
                    part=part,
                )
            )

        return result.onlyTrue()

    def axis_movement_and_intersections_observer(
        self,
        positionVector: VectorModel,
        alongAxis: str,
        objectIndentation: float,
        part,
    ) -> bool:
        # UP
        positionVector.__setattr__(
            alongAxis, positionVector.__getattribute__(alongAxis) + objectIndentation
        )
        self.objectSetPosition(part, positionVector.toFreeCadVector())
        result = self.isObjectIntersections(part=part)
        if result:
            return True
        # DOWN
        positionVector.__setattr__(
            alongAxis, positionVector.__getattribute__(alongAxis) - objectIndentation
        )
        positionVector.__setattr__(
            alongAxis, positionVector.__getattribute__(alongAxis) - objectIndentation
        )
        result = self.isObjectIntersections(part=part)
        if result:
            return True
        self.isObjectIntersections(part=part)
        # ROLLBACK
        positionVector.__setattr__(
            alongAxis, positionVector.__getattribute__(alongAxis) + objectIndentation
        )
        self.objectSetPosition(part, positionVector.toFreeCadVector())
        return False

    def getAllSolids(self):
        if self._solids.__len__() == 0:
            for part in App.ActiveDocument.Objects:
                if self.is_object_solid(part):
                    self._solids.append(part)

        return self._solids

    def is_object_solid(self, obj):
        if not isinstance(obj, App.DocumentObject):
            return False
        if hasattr(obj, "Group"):
            return False

        if not hasattr(obj, "Shape"):
            return False
        if not hasattr(obj.Shape, "Solids"):
            return False

        if len(obj.Shape.Solids) == 0:
            return False

        return True


T = TypeVar("T")


def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
    assert isinstance(x, list)
    return [f(y) for y in x]


def from_str(x: Any) -> str:
    assert isinstance(x, str)
    return x


def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]:
    assert isinstance(x, dict)
    return {k: f(v) for (k, v) in x.items()}


def to_class(c: Type[T], x: Any) -> dict:
    assert isinstance(x, c)
    return cast(Any, x).to_dict()


# Вспомогательный класс который делает генрацию JSON на основе пайтон обьектов


class AdjacencyMatrix:
    matrixError: Dict[str, str] = {}
    all_parts: List[str]
    first_detail: str
    matrix: Dict[str, List[str]]

    def __init__(
        self, all_parts: List[str], first_detail: str, matrix: Dict[str, List[str]]
    ) -> None:
        self.all_parts = all_parts
        self.first_detail = first_detail
        self.matrix = matrix
        self.validateMatrix()

    def whatPlaceLeadingPartIndex(self):
        i = 0
        for el in self.matrix:
            if el == self.first_detail:
                return i
            i = +1

    def validateMatrix(self):
        for el in self.all_parts:
            if self.matrix.get(el) == None:
                self.matrixError[el] = "Not found adjacency " + el

    @staticmethod
    def from_dict(obj: Any) -> "AdjacencyMatrix":
        assert isinstance(obj, dict)
        all_pars = from_list(from_str, obj.get("allParts"))
        first_detail = from_str(obj.get("firstDetail"))
        matrix = from_dict(lambda x: from_list(from_str, x), obj.get("matrix"))

        return AdjacencyMatrix(all_pars, first_detail, matrix)

    def to_dict(self) -> dict:
        result: dict = {}
        result["allParts"] = from_list(from_str, self.all_parts)
        result["firstDetail"] = from_str(self.first_detail)
        result["matrix"] = from_dict(lambda x: from_list(from_str, x), self.matrix)
        if self.matrixError.values().__len__() == 0:
            result["matrixError"] = None
        else:
            result["matrixError"] = self.matrixError
        return result

    def getDictMatrix(self) -> dict:
        result = {}

        for k, v in self.matrix.items():
            result[k] = {}
            for el in v:
                result[k][el] = el

        return result


def adjacency_matrix_from_dict(s: Any) -> AdjacencyMatrix:
    return AdjacencyMatrix.from_dict(s)


def adjacency_matrix_to_dict(x: AdjacencyMatrix) -> Any:
    return to_class(AdjacencyMatrix, x)


# Вспомогательный класс для работы с Freecad


class FreeCadMetaModel(object):
    def __init__(self, label, vertex) -> None:
        self.label = label
        self.vertex = vertex


collision_squares_labels = []


class MeshGeometryCoordinateModel(object):
    # Получение геометрии мешей
    def __init__(
        self,
        x,
        y,
        z,
        label,
    ):
        self.x = x
        self.y = y
        self.z = z
        self.label = label
        self.cadLabel = ""

    def initializePrimitivesByCoordinate(self, detailSquares):
        uuidDoc = str(uuid.uuid1())
        App.ActiveDocument.addObject("Part::Box", "Box")
        App.ActiveDocument.ActiveObject.Label = uuidDoc
        App.ActiveDocument.recompute()
        part = App.ActiveDocument.getObjectsByLabel(uuidDoc)[0]
        collision_squares_labels.append(uuidDoc)
        part.Width = 2
        part.Height = 2
        part.Length = 2
        part.Placement = App.Placement(
            App.Vector(self.x - 1, self.y - 1, self.z - 1),
            App.Rotation(App.Vector(0.00, 0.00, 1.00), 0.00),
        )
        if detailSquares.get(self.label) is None:
            detailSquares[self.label] = []
        detailSquares[self.label].append(self)
        self.cadLabel = uuidDoc
        App.ActiveDocument.recompute()


class FS:
    def readJSON(path: str):
        return json.loads((open(path)).read())

    def writeFile(data, filePath, fileName):
        file_to_open = filePath + fileName

        f = open(file_to_open, "w", encoding="utf8")

        f.write(data)

    def readFile(path: str):
        return open(path).read()

    def readFilesTypeFolder(pathFolder: str, fileType=".json"):
        filesJson = list(
            filter(
                lambda x: x[-fileType.__len__() :] == fileType, os.listdir(pathFolder)
            )
        )
        return filesJson


class GetAllPartsLabelsUseCase:
    # Получение всех названий деталей
    def call(self):
        parts = []
        for part in FreeCadRepository().getAllSolids():
            parts.append(part.Label)
        return parts


def isUnique(array, element):
    for i in array:
        if i == element:
            return False

    return True


class GetCollisionAtPrimitiveUseCase(object):
    # Получение колизий примитивов
    def call(self, freeCadMetaModels, detailSquares) -> Dict[str, List[str]]:
        matrix: Dict[str, List[str]] = {}
        for model in freeCadMetaModels:
            activePart = App.ActiveDocument.getObjectsByLabel(model.label)[0]
            for key in detailSquares:
                if model.label != key:
                    for renderPrimitive in detailSquares[key]:
                        primitivePart = App.ActiveDocument.getObjectsByLabel(
                            renderPrimitive.cadLabel
                        )[0]
                        collisionResult: int = int(
                            activePart.Shape.distToShape(primitivePart.Shape)[0]
                        )
                        if collisionResult == 0:
                            if matrix.get(model.label) == None:
                                matrix[model.label] = [renderPrimitive.label]
                            else:
                                if isUnique(matrix[model.label], renderPrimitive.label):
                                    matrix[model.label].append(renderPrimitive.label)
        return matrix


class GetFirstDetailUseCase:
    # Получение первой детали
    def call(self):
        return FreeCadRepository().getAllSolids()[0].Label


class GetPartPrimitiveCoordinatesUseCase(object):
    # Получение координат примитивов
    def call(self, freeCadMetaModels):
        meshCoordinates: list[MeshGeometryCoordinateModel] = []
        for model in freeCadMetaModels:
            vertexesDetail = model.vertex
            labelDetail = model.label
            for coords in vertexesDetail:
                detailVertex = MeshGeometryCoordinateModel(
                    coords.X,
                    coords.Y,
                    coords.Z,
                    labelDetail,
                )
                meshCoordinates.append(detailVertex)

        return meshCoordinates


class InitPartsParseUseCase:
    #  Инициализация парсинга
    def call(self):
        product_details = []
        for part in FreeCadRepository().getAllSolids():
            if part is not None:
                model = FreeCadMetaModel(part.Label, part.Shape.Vertexes)
                if model is not None:
                    product_details.append(model)
        return product_details


class RenderPrimitiveUseCase(object):
    #  Рендеринг премитивов
    def call(
        self, meshModels: list[MeshGeometryCoordinateModel], detailSquares
    ) -> None:
        for mesh in meshModels:
            mesh.initializePrimitivesByCoordinate(detailSquares)


class ClearWorkSpaceDocumentUseCase(object):
    # Очистка рабочего простарнства
    def call(self, detailSquares):
        for key in detailSquares:
            for renderPrimitive in detailSquares[key]:
                primitivePart = App.ActiveDocument.getObjectsByLabel(
                    renderPrimitive.cadLabel
                )[0]
                App.ActiveDocument.removeObject(primitivePart.Name)


class RenderPrimitivesScenario(object):
    def __init__(
        self,
        initPartsParseUseCase: InitPartsParseUseCase,
        getPartPrimitiveCoordinatesUseCase: GetPartPrimitiveCoordinatesUseCase,
        renderPrimitiveUseCase: RenderPrimitiveUseCase,
        getCollisionAtPrimitives: GetCollisionAtPrimitiveUseCase,
        clearWorkSpaceDocument: ClearWorkSpaceDocumentUseCase,
    ) -> None:
        self.initPartsParseUseCase = initPartsParseUseCase
        self.getPartPrimitiveCoordinatesUseCase = getPartPrimitiveCoordinatesUseCase
        self.renderPrimitiveUseCase = renderPrimitiveUseCase
        self.getCollisionAtPrimitives = getCollisionAtPrimitives
        self.clearWorkSpaceDocument = clearWorkSpaceDocument

    def call(self) -> None:
        meshCoordinates = []
        detailSquares = {}
        parts = self.initPartsParseUseCase.call()
        meshCoordinates = self.getPartPrimitiveCoordinatesUseCase.call(parts)
        self.renderPrimitiveUseCase.call(meshCoordinates, detailSquares)
        matrix = self.getCollisionAtPrimitives.call(parts, detailSquares)
        self.clearWorkSpaceDocument.call(detailSquares)
        return matrix


class ClearWorkSpaceDocumentUseCase(object):
    # Очистака рабочего пространства
    def call(self, detailSquares):
        for key in detailSquares:
            for renderPrimitive in detailSquares[key]:
                primitivePart = App.ActiveDocument.getObjectsByLabel(
                    renderPrimitive.cadLabel
                )[0]
                App.ActiveDocument.removeObject(primitivePart.Name)


class CadAdjacencyMatrix:
    # Матрица основанная на соприкосновении примитива с обьектами
    def primitiveMatrix(self):
        # Получение матрицы
        matrix = RenderPrimitivesScenario(
            InitPartsParseUseCase(),
            GetPartPrimitiveCoordinatesUseCase(),
            RenderPrimitiveUseCase(),
            GetCollisionAtPrimitiveUseCase(),
            ClearWorkSpaceDocumentUseCase(),
        ).call()
        return AdjacencyMatrix(
            all_parts=GetAllPartsLabelsUseCase().call(),
            first_detail=GetFirstDetailUseCase().call(),
            matrix=matrix,
        )

    # Матрица основанная на соприкосновении обьектов

    def matrixBySurfaces(self):
        matrix = {}
        for part in FreeCadRepository().getAllSolids():
            matrix[part.Label] = []
            for nextPart in FreeCadRepository().getAllSolids():
                if part.Label != nextPart.Label:
                    # Вычисление соприконсоновения площади деталей
                    collisionResult: int = int(
                        part.Shape.distToShape(nextPart.Shape)[0]
                    )
                    # print(collisionResult)
                    # print("collisionResult")
                    if collisionResult == 0:
                        matrix[part.Label].append(nextPart.Label)

        return AdjacencyMatrix(
            all_parts=GetAllPartsLabelsUseCase().call(),
            first_detail=GetFirstDetailUseCase().call(),
            matrix=matrix,
        )


def reduce(function, iterable, initializer=None):
    it = iter(iterable)
    if initializer is None:
        value = next(it)
    else:
        value = initializer
    for element in it:
        value = function(value, element)
    return value


def to_ascii_hash(text):
    ascii_values = [ord(character) for character in text]
    return reduce(lambda x, y: x + y, ascii_values)


def matrixGetUniqueContact(matrix):
    detailsToCheck = []
    detailsHashCheck = {}
    for k, v in matrix.items():
        for el in v:
            if el != k:
                hash = to_ascii_hash(k + el)
                if detailsHashCheck.get(hash) == None:
                    detailsHashCheck[hash] = hash
                    detailsToCheck.append({"child": el, "parent": k})
    return detailsToCheck


class IntersectionComputedUseCase:
    def call(parts):
        App.activeDocument().addObject("Part::MultiCommon", "Common")
        App.activeDocument().Common.Shapes = [parts[0], parts[1]]
        App.activeDocument().getObject("Common").ViewObject.ShapeColor = getattr(
            parts[0].getLinkedObject(True).ViewObject,
            "ShapeColor",
            App.activeDocument().getObject("Common").ViewObject.ShapeColor,
        )
        App.activeDocument().getObject("Common").ViewObject.DisplayMode = getattr(
            parts[0].getLinkedObject(True).ViewObject,
            "DisplayMode",
            App.activeDocument().getObject("Common").ViewObject.DisplayMode,
        )
        App.ActiveDocument.recompute()
        area = App.activeDocument().getObject("Common").Shape.Area
        App.ActiveDocument.removeObject("Common")
        return area


class ErrorStringModel:
    def __init__(self, error: str) -> None:
        self.error = error
        pass

    error: str

    def toString(self) -> str:
        return json.dumps(
            {
                "error": self.error,
            },
            ensure_ascii=False,
            indent=4,
        )


class IsAllObjectSolidsCheckUseCase:
    def call() -> ErrorStringModel:
        result = FreeCadRepository().isAllObjectsSolids()
        if result.__len__() == 0:
            return None

        return ErrorStringModel(error="Is not solid objects: " + ",".join(result))


class CheckObjectHasTouchesUseCase:
    def call(objectIndentation: float) -> ErrorStringModel:
        result = []
        for part in FreeCadRepository().getAllSolids():
            if (
                FreeCadRepository().objectHasTouches(
                    part=part, objectIndentation=objectIndentation
                )
                is False
            ):
                result.append(part.Label)
        if result.__len__() == 0:
            return None
        return ErrorStringModel(
            error="Solids bodies have no recounts: " + ",".join(result)
        )


class CheckCadIntersectionObjects:
    report = []

    def call() -> bool:
        FreeCadRepository().getAllSolids()
        return False


class ExitFreeCadUseCase:
    def call():
        import FreeCADGui as Gui

        freecadQTWindow = Gui.getMainWindow()
        freecadQTWindow.close()


# class CheckValidIntersectionUseCase:
#     def call() -> ErrorStringModel:
#         for part in FreeCadRepository().getAllSolids():
#             print(part)
#             FreeCadRepository().obj
#         pass

#функция, проверяющая, открывается ли программа через консоль или через верстак freecad. 
def get_paths():
    if 'FreeCAD' in globals():
        active_doc = FreeCAD.activeDocument()
        if active_doc:
            cadFilePath = active_doc.FileName
            outPath = os.path.dirname(cadFilePath)
        else:
            raise Exception("Нет активного документа в FreeCAD.")
    else:
        with open('env.json', 'r', encoding='utf-8') as file:
            env_data = json.load(file)
            cadFilePath = env_data.get('cadFilePath')
            outPath = env_data.get('outPath')
            if not cadFilePath or not outPath:
                raise Exception("Не найдены cadFilePath или outPath в env.json.")
    
    return cadFilePath, outPath



def main():
    if 'FreeCAD' in globals():
        active_doc = FreeCAD.activeDocument()
        if active_doc:
            cadFilePath = active_doc.FileName
            outPath = os.path.dirname(cadFilePath)
        else:
            # raise Exception("Нет активного документа в FreeCAD.")
        # else:
            env = FS.readJSON("env.json")
            print(env)
            cadFilePath = str(env["cadFilePath"])
            outPath = str(env["outPath"])
            objectIndentation = float(env["objectIndentation"])

            if cadFilePath == None:
                return TypeError("CadFile not found env.json")
            App.open("" + cadFilePath)

            isAllObjectSolidsCheckUseCase = IsAllObjectSolidsCheckUseCase.call()

            if isAllObjectSolidsCheckUseCase != None:
                FS.writeFile(isAllObjectSolidsCheckUseCase.toString(), outPath, 'error.json')
                ExitFreeCadUseCase.call()
                return
            
            FreeCAD.open(cadFilePath)
    


    topologyMatrix = CadAdjacencyMatrix().matrixBySurfaces()

    sequences = {
        "sequences": AllSequences(topologyMatrix.matrix).adj_matrix_names
    }
    matrix = topologyMatrix.matrix
    contacts = matrixGetUniqueContact(matrix)
    intersection_geometry = {"status": True, "recalculations": None}
    
    for el in contacts:
        child = App.ActiveDocument.getObjectsByLabel(el.get("child"))[0]
        parent = App.ActiveDocument.getObjectsByLabel(el.get("parent"))[0]
        area = IntersectionComputedUseCase.call([child, parent])
        if area != 0.0:
            if intersection_geometry.get("recalculations") is None:
                intersection_geometry["status"] = False
                intersection_geometry["recalculations"] = []
            intersection_geometry["recalculations"].append(
                {"area": area, "connect": el.get("child") + " " + el.get("parent")}
            )
    # print(intersection_geometry, sequences, topologyMatrix.to_dict())
    return intersection_geometry, sequences, topologyMatrix.to_dict()

    # ExitFreeCadUseCase.call() Сейчас пока не нужна
    # return intersection_geometry, sequences, topologyMatrix.to_dict()


# main()
