'''
参考资料：
https://www.gnu.org/software/hp2xx/hp2xx.html
https://zhuanlan.zhihu.com/p/622090369
'''

import os
import typing
import string
import re
import warnings
from enum import Enum
import shapely
import shapely.ops
from shapely.geometry import LineString, Polygon, MultiPolygon, MultiLineString
import numpy as np
import cv2
import itertools
from shapely.validation import make_valid


def my_warning_handler(message, category, filename, lineno, file=None, line=None):
    print(f"PLT警告: {message}")
warnings.showwarning = my_warning_handler

def assert_warning(condition, message):
    if not condition:
        warnings.warn(message, UserWarning)

class PltCommand(Enum):
    IN = 0
    PU = 1
    PD = 2
    PA = 3

AbstractSyntaxTree = typing.List[typing.Dict]


class VirtualMachineOutput(object):
    '''虚拟机执行后的结果'''
    MIN_FILTER_AREA_FACTOR = 0.0068 ** 2
    MAX_FILTER_AREA_FACTOR = 0.8 ** 2

    def __init__(self, polygons :typing.List[Polygon], dispensable: typing.List[LineString]) -> None:
        for poly in polygons:
            assert isinstance(poly, Polygon)
        for line in dispensable:
            assert isinstance(line, LineString)

        # 过滤结果

        # 计算画布大小
        min_x, min_y, max_x, max_y = MultiPolygon(polygons).bounds

        filter_polygons = []
        for polygon in polygons:
            if (max_x - min_x) * (max_y - min_y) * VirtualMachineOutput.MIN_FILTER_AREA_FACTOR < polygon.area < (max_x - min_x) * (max_y - min_y) *  VirtualMachineOutput.MAX_FILTER_AREA_FACTOR:
                filter_polygons.append(polygon)
            else:
                boundary = polygon.boundary
                if isinstance(boundary, LineString):
                    dispensable.append(boundary)
                elif isinstance(boundary, MultiLineString):
                    dispensable.extend(list(boundary.geoms))

        print(f"过滤了{len(polygons) - len(filter_polygons)}个多边形")
        polygons = filter_polygons

        # 计算多边形的包含关系，构建多边形树
        self.nodes = [{"data":polygon, "child":[], "parent":None, "index":index} for index, polygon in enumerate(polygons)]
        self.dispensable = dispensable

        # 找每个node的最佳parent
        for node1 in self.nodes:
            best_parent = None
            for node2 in self.nodes:
                if node1 == node2:
                    continue

                if node1["index"] == 8 and node2["index"] == 9:
                    pass

                # 判断是否包含
                #if node2["data"].contains(node1["data"]):
                if node2["data"].area > node1["data"].area:
                    intersection = node2["data"].intersection(node1["data"])
                    if intersection.area > node1["data"].area * 0.99:
                        if best_parent is None:
                            best_parent = node2
                        else:
                            if best_parent["data"].contains(node2["data"]):
                                best_parent = node2

            node1["parent"] = best_parent

        # 填充child
        self.tree = []
        for node in self.nodes:
            if node["parent"] is not None:
                node["parent"]["child"].append(node)
            else:
                self.tree.append(node)

        # 构建广度优先遍历
        self.bfs = self.tree.copy()
        index = 0
        while index < len(self.bfs):
            self.bfs.extend(self.bfs[index]["child"])
            index += 1

    def _draw_nodes(self, nodes: typing.List, scale_factor: float, show_id: bool = True) -> np.ndarray:
        # 计算画布大小
        min_x, min_y, max_x, max_y = MultiPolygon([node["data"] for node in nodes]).bounds

        max_x = int(np.ceil(max_x * scale_factor))
        max_y = int(np.ceil(max_y * scale_factor))
        min_x = int(np.floor(min_x * scale_factor))
        min_y = int(np.floor(min_y * scale_factor))

        max_x -= min_x
        max_y -= min_y

        #print("画布大小", max_x, max_y)

        result_img = np.zeros((max_y, max_x, 4), dtype=np.uint8)

        nodes = sorted(nodes, key=lambda node: self.bfs.index(node))

        for node in nodes:
            pts = (np.asarray(node["data"].exterior.coords) * scale_factor - [min_x, min_y]).astype(np.int32)
            pts[:,1] = max_y - pts[:,1]
            cv2.fillPoly(result_img, [pts], (255,255,255,255))
            
            # 最外圈（parent为None）用红色，内部用黑色
            if node["parent"] is None:
                cv2.polylines(result_img, [pts], True, (0,0,255,255), 2)  # 红色 (BGR: 0,0,255)
            else:
                cv2.polylines(result_img, [pts], True, (0,0,0,255), 2)    # 黑色

        # 画无关紧要的东西（dispensable线段）
        multiPolygon = MultiPolygon([node["data"] for node in nodes])
        multiPolygon = make_valid(multiPolygon.buffer(0))
        
        # 获取最外圈轮廓（parent为None的节点）
        outer_contours = [node["data"].exterior for node in nodes if node["parent"] is None]
            
        for shape in self.dispensable:
            if multiPolygon.contains(shape):
                pts = (np.asarray(shape.coords) * scale_factor - [min_x, min_y]).astype(np.int32)
                pts[:,1] = max_y - pts[:,1]
                
                # 判断是否与最外圈轮廓粘连（距离非常近）
                is_connected_to_outer = False
                line_length = shape.length  # 线段长度（PLT单位）
                
                # 距离阈值（PLT单位），小于此值视为粘连
                CONNECT_THRESHOLD = 1
                # 线段长度限制（PLT单位）
                MAX_LINE_LENGTH = 155
                
                if line_length < MAX_LINE_LENGTH:  # 只检查短线段
                    for outer in outer_contours:
                        distance = shape.distance(outer)
                        if distance < CONNECT_THRESHOLD:
                            is_connected_to_outer = True
                            break
                
                if is_connected_to_outer:
                    # ========== 调试开关 ==========
                    # True = 蓝色标记模式（用于调试查看哪些线被检测到）
                    # False = 镂空模式（正式使用，挖空0.2cm宽度）
                    DEBUG_MODE = False
                    
                    if DEBUG_MODE:
                        # 调试模式：蓝色标记，方便查看检测结果
                        cv2.polylines(result_img, [pts], False, (255,0,0,255), 2)  # 蓝色加粗
                    else:
                        # 正式模式：镂空效果（方形端点）
                        # 用矩形多边形代替polylines，避免圆形端点
                        cutout_half_width = int(15 * scale_factor)  # 半宽度
                        if cutout_half_width < 1:
                            cutout_half_width = 1
                        
                        # 对每个线段创建矩形多边形
                        for i in range(len(pts) - 1):
                            p1 = pts[i]
                            p2 = pts[i + 1]
                            
                            # 计算线段方向的垂直向量
                            dx = p2[0] - p1[0]
                            dy = p2[1] - p1[1]
                            length = np.sqrt(dx*dx + dy*dy)
                            if length < 0.001:
                                continue
                                
                            # 垂直方向单位向量
                            nx = -dy / length * cutout_half_width
                            ny = dx / length * cutout_half_width
                            
                            # 构建矩形的四个角点
                            rect_pts = np.array([
                                [p1[0] + nx, p1[1] + ny],
                                [p1[0] - nx, p1[1] - ny],
                                [p2[0] - nx, p2[1] - ny],
                                [p2[0] + nx, p2[1] + ny]
                            ], dtype=np.int32)
                            
                            # 用透明色填充矩形
                            cv2.fillPoly(result_img, [rect_pts], (0,0,0,0))
                else:
                    cv2.polylines(result_img, [pts], False, (0,0,0,255), 1)    # 黑色

        # 画parent为None的id
        if show_id:
            for node in nodes:
                if node["parent"] is None:
                    centroid = node["data"].centroid
                    text_pos = (int((centroid.x * scale_factor) - min_x), int(max_y - (centroid.y * scale_factor) - min_y))
                    cv2.putText(result_img, str(node["index"]), text_pos, cv2.FONT_HERSHEY_SIMPLEX, 3, (255,0,0,255), 5)

        return result_img

    def full_sheet(self, scale_factor: float = 0.1) -> np.ndarray:
        '''整幅输出'''
        return self._draw_nodes(self.bfs, scale_factor)

    def debug_full_sheet(self, scale_factor: float = 0.1) -> np.ndarray:
        """
        用随机颜色画出所有多边形和被过滤掉的线段
        """
        # 计算画布大小，画布大小应该考虑dispensable
        all_shapes = [node["data"] for node in self.nodes] + self.dispensable
        temp = []
        for shape in all_shapes:
            if isinstance(shape, LineString):
                convex_hull = shape.convex_hull
                if isinstance(convex_hull, Polygon):
                    temp.append(convex_hull)
            else:
                temp.append(shape)
        min_x, min_y, max_x, max_y = MultiPolygon(temp).bounds
        max_x = int(np.ceil(max_x * scale_factor))
        max_y = int(np.ceil(max_y * scale_factor))
        min_x = int(np.floor(min_x * scale_factor))
        min_y = int(np.floor(min_y * scale_factor))
        max_x -= min_x
        max_y -= min_y

        result_img = np.zeros((max_y, max_x, 4), dtype=np.uint8)

        # 画多边形
        for node in self.nodes:
            #overlay = result_img.copy()
            color = tuple(np.random.randint(0,256,size=3).tolist() + [255])
            pts = (np.asarray(node["data"].exterior.coords) * scale_factor - [min_x, min_y]).astype(np.int32)
            pts[:,1] = max_y - pts[:,1]
            cv2.fillPoly(result_img, [pts], color)
            if node["parent"] is None:
                cv2.polylines(result_img, [pts], True, (0,0,255,255), 1)
            else:
                cv2.polylines(result_img, [pts], True, (0,0,0,255), 1)
            #alpha = 0.5
            #cv2.addWeighted(overlay, alpha, result_img, 1 - alpha, 0, result_img)

        # 画被过滤掉的线段
        for shape in self.dispensable:
            #overlay = result_img.copy()
            color = tuple(np.random.randint(0,256,size=3).tolist() + [255])
            pts = (np.asarray(shape.coords) * scale_factor - [min_x, min_y]).astype(np.int32)
            pts[:,1] = max_y - pts[:,1]
            cv2.polylines(result_img, [pts], False, color, 1)
            #cv2.addWeighted(overlay, alpha, result_img, 1 - alpha, 0, result_img)

        # 画parent为None的id
        for node in self.nodes:
            if node["parent"] is None:
                centroid = node["data"].centroid
                text_pos = (int((centroid.x * scale_factor) - min_x), int(max_y - (centroid.y * scale_factor) - min_y))
                cv2.putText(result_img, str(node["index"]), text_pos, cv2.FONT_HERSHEY_SIMPLEX, 3, (0,0,255,255), 5)


        return result_img

    def _distance_between_cluster(self, cluster1, cluster2):
        min_distance = float("inf")
        for node1 in cluster1:
            for node2 in cluster2:
                assert isinstance(node1["data"], Polygon)
                assert isinstance(node2["data"], Polygon)

                d = node1["data"].distance(node2["data"])
                if d < min_distance:
                    min_distance = d
        return min_distance

    def get_single_size_info(self, size_num: int) -> typing.List[typing.List]:
        '''获取单码信息'''
        # 距离聚类
        clusters = [[node] for node in self.nodes]

        while len(clusters) > size_num:
            # 每个循环将合并距离最小的两个类
            min_distance_clusters = (None, None)
            min_distance = float("inf")
            for cluster1, cluster2 in itertools.combinations(clusters, 2):
                d = self._distance_between_cluster(cluster1, cluster2)
                if d < min_distance:
                    min_distance = d
                    min_distance_clusters = (cluster1, cluster2)

            clusters.remove(min_distance_clusters[0])
            clusters.remove(min_distance_clusters[1])
            clusters.append(min_distance_clusters[0] + min_distance_clusters[1])

        # 排序
        sorted_clusters = sorted(clusters, key=lambda x: min([node["index"] for node in x]))

        return list(sorted_clusters)

    def single_size(self, size_num: int, scale_factor: float = 0.1) -> typing.List[np.ndarray]:
        '''单码输出'''
        sorted_clusters = self.get_single_size_info(size_num)

        result = []
        for cluster in sorted_clusters:
            result.append(self._draw_nodes(cluster, scale_factor))
        return result

    def single_piece(self, scale_factor: float = 0.1, show_id: bool = False) -> typing.List[typing.Tuple[int, np.ndarray]]:
        '''单片输出
        返回: [(裁片ID, 图像), ...]
        '''
        result = []
        for node in self.nodes:
            # 如果是顶层则新建一副图像
            if node["parent"] is None:
                nodes = [node] + node['child']
                img = self._draw_nodes(nodes, scale_factor, show_id=show_id)
                result.append((node["index"], img))
        return result


class _VirtualMachine(object):
    '''运行PLT文件的虚拟机'''
    def __init__(self, tolerance) -> None:
        self.tolerance = tolerance

    def run(self, abstract_syntax_tree: AbstractSyntaxTree) -> VirtualMachineOutput:
        context = abstract_syntax_tree.copy()
        current_x = 0
        current_y = 0
        self.pen_state = 'UP'
        lines :typing.List[typing.List[typing.Tuple]] = []
        lines_flag :typing.List[int] = []

        while len(context) != 0:
            code = context[0]

            try:
                if code["cmd"] == PltCommand.IN:
                    print("初始化")

                elif code["cmd"] == PltCommand.PA:
                    if self.pen_state == 'UP':
                        pass
                    elif self.pen_state == 'DOWN':
                        new_x = float(code["args"][0])
                        new_y = float(code["args"][1])

                        # 判断是否与已有的线相连接
                        for i in range(len(lines) - 1, -1, -1):
                            if lines_flag[i] == 1:
                                continue
                            if lines[i][0] == (current_x, current_y):
                                lines[i].insert(0, (new_x, new_y))
                                if lines[i][0] == lines[i][-1]:
                                    lines_flag[i] = 1
                                break
                            elif lines[i][0] == (new_x, new_y):
                                lines[i].insert(0, (current_x, current_y))
                                if lines[i][0] == lines[i][-1]:
                                    lines_flag[i] = 1
                                break
                            elif lines[i][-1] == (current_x, current_y):
                                lines[i].append((new_x, new_y))
                                if lines[i][0] == lines[i][-1]:
                                    lines_flag[i] = 1
                                break
                            elif lines[i][-1] == (new_x, new_y):
                                lines[i].append((current_x, current_y))
                                if lines[i][0] == lines[i][-1]:
                                    lines_flag[i] = 1
                                break
                        else:
                            # 不与已有的线相连接
                            lines.append([(current_x, current_y), (new_x, new_y)])
                            lines_flag.append(0)

                    current_x = float(code["args"][0])
                    current_y = float(code["args"][1])

                elif code["cmd"] == PltCommand.PU:
                    self.pen_state = 'UP'

                elif code["cmd"] == PltCommand.PD:
                    self.pen_state = 'DOWN'

            except Exception as e:
                assert_warning(False, f"[code {code}] 执行指令异常: {e}")
            finally:
                context = context[1:]

        # 创建LineString :typing.List[LineString] = []
        lineStrings :typing.List[LineString] = []
        lineStrings = [LineString(line) for line in lines if len(set(line)) > 1]

        '''
        import matplotlib.pyplot as plt
        for line in lineStrings:
            x, y = line.xy
            plt.plot(x, y)
        plt.show()
        '''

        return self.__build_polygons_and_dispensable(lineStrings)

    def __build_polygons_and_dispensable(self, lineStrings: typing.List[LineString]) -> VirtualMachineOutput:

        # 为非常近的线搭桥
        tree = shapely.STRtree(lineStrings)

        bridges = []
        # 遍历所有线段的端点，寻找需要搭桥的地方
        for line in lineStrings:
            endpoints = [shapely.Point(line.coords[0]), shapely.Point(line.coords[-1])]

            for p in endpoints:
                for idx in tree.query(p.buffer(self.tolerance)):
                    other_line = lineStrings[idx]
                    if other_line == line:
                        continue

                    dist = p.distance(other_line)

                    if 0 < dist <= self.tolerance:
                        _, p_target = shapely.ops.nearest_points(p, other_line)
                        bridge = LineString([p, p_target])
                        bridges.append(bridge)

        lineStrings.extend(bridges)

        # 节点化并提取闭合区域
        all_shapes = shapely.ops.unary_union(lineStrings)
        assert isinstance(all_shapes, MultiLineString)
        polygons :typing.List[Polygon] = list(shapely.ops.polygonize(all_shapes))

        # 合并重叠多边形
        merged_polygons = shapely.unary_union(polygons)
        assert isinstance(merged_polygons, MultiPolygon)
        polygons = list(merged_polygons.geoms)

        # 只保留外环
        polygons = [Polygon(poly.exterior) for poly in polygons]

        # 找到多余的线段
        dispensable :typing.List[LineString] = []
        closed_rings = [poly.exterior for poly in polygons]
        if closed_rings:
            leftovers = all_shapes.difference(shapely.ops.unary_union(closed_rings))
            assert isinstance(leftovers, shapely.MultiLineString)
            for shape in leftovers.geoms:
                dispensable.append(shape)
        else:
            dispensable = list(all_shapes.geoms)

        for i in range(len(polygons)):
            polygons[i] = polygons[i].buffer(0)

        print(f"共生成多边形{len(polygons)}个，线段{len(dispensable)}条")

        output = VirtualMachineOutput(polygons, dispensable)

        return output

class _Preprocessor(object):
    '''PLT文件预处理器'''
    def __init__(self) -> None:
        pass

    def get_abstract_syntax_tree(self, src_content: str) -> AbstractSyntaxTree:
        content = src_content

        # 换行符转分号 某些文件的语句末尾没有分号
        content = content.replace("\n", ";")
        content = content.replace(" ", ",")

        # 删除所有空白字符
        for space in string.whitespace:
            content = content.replace(space, "")

        result = []

        # 遍历每一行命令
        for line_index, line in enumerate(content.split(";")):
            if line == "":
                continue

            # 提取开头的英文
            pattern = r'^[a-zA-Z]+'
            match = re.search(pattern, line)

            if match is None:
                assert_warning(False, f"[line {line_index}] 未找到命令: {line}")
                continue

            # 获取对应的指令
            command_str = match.group().upper()
            try:
                command = PltCommand[command_str]
            except KeyError:
                assert_warning(False, f"[line {line_index}] 不支持的指令: {command_str}")
                continue

            # 获取参数
            if len(command_str) == len(line):
                stack = []
            else:
                stack = line[len(command_str):].split(",")

            if command == PltCommand.IN:
                result.append({'cmd': PltCommand.IN, 'args': []})

            elif command == PltCommand.PA:
                for i in range(len(stack) // 2):
                    result.append({'cmd': PltCommand.PA, 'args': [stack[0], stack[1]]})
                    stack = stack[2:]

            elif command == PltCommand.PU:
                result.append({'cmd': PltCommand.PU, 'args': []})
                for i in range(len(stack) // 2):
                    result.append({'cmd': PltCommand.PA, 'args': [stack[0], stack[1]]})
                    stack = stack[2:]

            elif command == PltCommand.PD:
                result.append({'cmd': PltCommand.PD, 'args': []})
                for i in range(len(stack) // 2):
                    result.append({'cmd': PltCommand.PA, 'args': [stack[0], stack[1]]})
                    stack = stack[2:]

            assert_warning(len(stack) == 0, f"[line {line_index}] 栈非空: {stack}")

        return result


class PltReader(object):
    '''PLT文件读取器'''

    def __init__(self, pltfile: typing.TextIO) -> None:
        self.pltfile = pltfile

        preprocessor = _Preprocessor()
        self.abstract_syntax_tree = preprocessor.get_abstract_syntax_tree(self.pltfile.read())

    def get_output(self, tolerance=5):
        machine = _VirtualMachine(tolerance=tolerance)
        output = machine.run(self.abstract_syntax_tree)
        return output


if __name__ == "__main__":
    filepath = "默认方案-POLO衫长袖-A料-标准.plt"

    with open(filepath, 'r') as f:
        reader = PltReader(f)
        output = reader.get_output()

    # 调试整幅输出（随机颜色）
    debug_full_sheet_path = "debug_full_sheet.png"
    cv2.imwrite(debug_full_sheet_path, output.debug_full_sheet(scale_factor=72/1016))
    print(f"调试整幅输出已写入: {debug_full_sheet_path}")

    # 整幅输出
    full_sheet_path = "full_sheet.png"
    cv2.imwrite(full_sheet_path, output.full_sheet(scale_factor=72/1016))
    print(f"整幅输出已写入: {full_sheet_path}")

    # 单码输出
    single_size_path = "single_size"
    os.makedirs(single_size_path, exist_ok=True)
    for index, img in enumerate(output.single_size(size_num=5, scale_factor=72/1016)):
        cv2.imwrite(os.path.join(single_size_path, f"size_{index}.png"), img)
    print(f"单码输出已写入: {single_size_path}/ (共{len(os.listdir(single_size_path))}个文件)")

    # 单片输出 - 每个裁片独立保存为PNG
    single_piece_path = "single_piece"
    os.makedirs(single_piece_path, exist_ok=True)
    pieces = output.single_piece(scale_factor=72/1016, show_id=False)
    for piece_id, img in pieces:
        cv2.imwrite(os.path.join(single_piece_path, f"piece_{piece_id}.png"), img)
    print(f"单片输出已写入: {single_piece_path}/ (共{len(pieces)}个裁片)")

