import subprocess
import time

import psutil
import pythoncom
import win32com.client
from enum import Enum

# project by jones peter

# Enum to represent common colors in AutoCAD
class Color(Enum):
    RED = 1
    YELLOW = 2
    GREEN = 3
    CYAN = 4
    BLUE = 5
    MAGENTA = 6
    WHITE = 7
    GRAY = 8
    ORANGE = 30
    PURPLE = 40
    BROWN = 41

    @staticmethod
    def from_name(name):
        try:
            return Color[name.upper()].value
        except KeyError:
            raise ValueError(f"Color '{name}' is not a valid color name")


# Enum to represent alignments in AutoCAD
class Alignment(Enum):
    LEFT = 'left'
    CENTER = 'center'
    RIGHT = 'right'


# Enum to represent dimension types in AutoCAD
class DimensionType(Enum):
    ALIGNED = 'aligned'
    LINEAR = 'linear'
    ANGULAR = 'angular'
    RADIAL = 'radial'
    DIAMETER = 'diameter'


# Enum to represent line styles in AutoCAD
class LineStyle(Enum):
    CONTINUOUS = 'Continuous'  # ------------
    DASHED = 'Dashed'  # - - - - - -
    DOTTED = 'Dotted'  # . . . . . .
    CENTER = 'Center'  # - . - . - .
    HIDDEN = 'Hidden'  # - - - - - -
    PHANTOM = 'Phantom'  # - . . - . .
    BREAK = 'Break'  # -     -
    BORDER = 'Border'  # - - - . - -
    DOT2 = 'Dot2'  # .  .  .  .
    DOTX2 = 'DotX2'  # .   .   .
    DIVIDE = 'Divide'  # -  .  -  .
    TRACKING = 'Tracking'  # - .  - .
    DASHDOT = 'Dashdot'  # - . - . -


# Class to represent a 3D point
class APoint:
    def __init__(self, x=0, y=0, z=0):
        self.x = x
        self.y = y
        self.z = z

    def to_variant(self):
        return win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [self.x, self.y, self.z])

    def to_tuple(self):
        return (self.x, self.y)

    def __repr__(self):
        return f"APoint({self.x}, {self.y}, {self.z})"


# Class to represent a layer
class Layer:
    def __init__(self, name, color=Color.WHITE, visible=True):
        self.name = name
        self.color = color
        self.visible = visible

    def __repr__(self):
        return f"Layer(name='{self.name}', color={self.color}, visible={self.visible})"


# Class to represent a block reference
class BlockReference:
    def __init__(self, name, insertion_point, scale=1.0, rotation=0.0):
        self.name = name
        self.insertion_point = insertion_point
        self.scale = scale
        self.rotation = rotation

    def __repr__(self):
        return f"BlockReference(name='{self.name}', insertion_point={self.insertion_point}, scale={self.scale}, rotation={self.rotation})"


# Class to represent a text object
class Text:
    def __init__(self, content, insertion_point, height, alignment=Alignment.LEFT):
        self.content = content
        self.insertion_point = insertion_point
        self.height = height
        self.alignment = alignment

    def __repr__(self):
        return f"Text(content='{self.content}', insertion_point={self.insertion_point}, height={self.height}, alignment='{self.alignment}')"


# Class to represent a dimension
class Dimension:
    def __init__(self, start_point, end_point, text_point, dimension_type=DimensionType.ALIGNED):
        self.start_point = start_point
        self.end_point = end_point
        self.text_point = text_point
        self.dimension_type = dimension_type

    def __repr__(self):
        return f"Dimension(start_point={self.start_point}, end_point={self.end_point}, text_point={self.text_point}, dimension_type='{self.dimension_type}')"


# Custom class for handling AutoCAD errors
class CADException(Exception):
    def __init__(self, message):
        super().__init__(message)
        # Additional error handling logic can be added here, such as logging to a file
        print(f"AutoCADError: {message}")


def is_autocad_installed():
    try:
        output = subprocess.check_output('wmic product get name', shell=True, stderr=subprocess.DEVNULL, text=True)
        return "AutoCAD" in output
    except subprocess.CalledProcessError:
        return False


def is_autocad_running():
    """Check if AutoCAD is running by looking for its process in the system."""
    # List of possible AutoCAD process names
    autocad_processes = ['acad.exe', 'AutoCAD', 'acad']
    for process in psutil.process_iter(['name']):
        # Check if the process name matches any of the AutoCAD-related names
        if any(autocad_process.lower() in process.info['name'].lower() for autocad_process in autocad_processes):
            return True
    return False


# Main class for interacting with AutoCAD
class AutoCAD:
    def __init__(self):
        try:
            # Creating COM client to AutoCad Application
            self.acad = win32com.client.Dispatch("AutoCAD.Application")
            self.acad.Visible = True
        except Exception as e:
            raise CADException(f"Error initializing AutoCAD: {e}")

    @property
    def doc(self):
        """ Returns `ActiveDocument` of current :attr:`Application`"""
        return self.acad.ActiveDocument

    @property
    def modelspace(self):
        """ Returns `ActiveDocument` of current :attr:`Application`"""
        return self.doc.ModelSpace

    def purge(self):
        """Purges all unused elements in the active document."""
        try:
            # Purge all named objects
            self.doc.PurgeAll()
            print("Successfully purged the document.")
        except Exception as e:
            raise CADException(f"Error purging AutoCAD document: {e}")

    # Iterate over objects in the model space, optionally filtering by object type
    def iter_objects(self, object_type=None):
        for obj in self.modelspace:
            if object_type is None or obj.EntityName == object_type:
                yield obj

    # Add a circle to the model space
    def add_circle(self, center, radius):
        try:
            circle = self.modelspace.AddCircle(center.to_variant(), radius)
            return circle
        except Exception as e:
            raise CADException(f"Error adding circle: {e}")

    # Add a line to the model space
    def add_line(self, start_point, end_point):
        try:
            line = self.modelspace.AddLine(start_point.to_variant(), end_point.to_variant())
            return line
        except Exception as e:
            raise CADException(f"Error adding line: {e}")

    # Add a rectangle to the model space
    def add_rectangle(self, lower_left, upper_right):
        try:
            x1, y1 = lower_left.to_tuple()
            x2, y2 = upper_right.to_tuple()
            points = [
                x1, y1,
                x2, y1,
                x2, y2,
                x1, y2,
                x1, y1
            ]
            points_variant = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, points)
            polyline = self.modelspace.AddLightweightPolyline(points_variant)
            return polyline
        except Exception as e:
            raise CADException(f"Error adding rectangle: {e}")

    # Add an ellipse to the model space
    def add_ellipse(self, center, major_axis, ratio):
        try:
            ellipse = self.modelspace.AddEllipse(center.to_variant(), major_axis.to_variant(), ratio)
            return ellipse
        except Exception as e:
            raise CADException(f"Error adding ellipse: {e}")

    # Add a text object to the model space
    def add_text(self, text):
        try:
            text_obj = self.modelspace.AddText(text.content, text.insertion_point.to_variant(), text.height)
            return text_obj
        except Exception as e:
            raise CADException(f"Error adding text: {e}")

    # Add a dimension to the model space
    def add_dimension(self, dimension):
        try:
            dimension_obj = None
            if dimension.dimension_type == DimensionType.ALIGNED:
                dimension_obj = self.modelspace.AddDimAligned(dimension.start_point.to_variant(),
                                                              dimension.end_point.to_variant(),
                                                              dimension.text_point.to_variant())
            elif dimension.dimension_type == DimensionType.LINEAR:
                dimension_obj = self.modelspace.AddDimLinear(dimension.start_point.to_variant(),
                                                             dimension.end_point.to_variant(),
                                                             dimension.text_point.to_variant())
            elif dimension.dimension_type == DimensionType.ANGULAR:
                dimension_obj = self.modelspace.AddDimAngular(dimension.start_point.to_variant(),
                                                              dimension.end_point.to_variant(),
                                                              dimension.text_point.to_variant())
            elif dimension.dimension_type == DimensionType.RADIAL:
                dimension_obj = self.modelspace.AddDimRadial(dimension.start_point.to_variant(),
                                                             dimension.end_point.to_variant(),
                                                             dimension.text_point.to_variant())
            elif dimension.dimension_type == DimensionType.DIAMETER:
                dimension_obj = self.modelspace.AddDimDiameter(dimension.start_point.to_variant(),
                                                               dimension.end_point.to_variant(),
                                                               dimension.text_point.to_variant())
            return dimension_obj
        except Exception as e:
            raise CADException(f"Error adding dimension: {e}")

    # Add a point to the model space
    def add_point(self, point):
        try:
            point_obj = self.modelspace.AddPoint(point.to_variant())
            return point_obj
        except Exception as e:
            raise CADException(f"Error adding point: {e}")

    # Add a polyline to the model space
    def add_polyline(self, points):
        try:
            points_variant = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8,
                                                     [coord for point in points for coord in point.to_tuple()])
            polyline = self.modelspace.AddLightweightPolyline(points_variant)
            return polyline
        except Exception as e:
            raise CADException(f"Error adding polyline: {e}")

    # Add a spline to the model space
    def add_spline(self, points):
        try:
            points_variant = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8,
                                                     [coord for point in points for coord in point.to_variant()])
            spline = self.modelspace.AddSpline(points_variant)
            return spline
        except Exception as e:
            raise CADException(f"Error adding spline: {e}")

    # Add an arc to the model space
    def add_arc(self, center, radius, start_angle, end_angle):
        try:
            arc = self.modelspace.AddArc(center.to_variant(), radius, start_angle, end_angle)
            return arc
        except Exception as e:
            raise CADException(f"Error adding arc: {e}")

    # Explode an object or a set of joined objects
    def explode_object(self, obj):
        try:
            exploded_items = obj.Explode()
            return exploded_items
        except Exception as e:
            raise CADException(f"Error exploding object: {e}")

    # Get maximum extents of a block
    def get_block_extents(self, block_name):
        try:
            for entity in self.iter_objects("AcDbBlockReference"):
                if entity.Name == block_name:
                    min_point = APoint(*entity.GeometricExtents.MinPoint)
                    max_point = APoint(*entity.GeometricExtents.MaxPoint)
                    return min_point, max_point
        except Exception as e:
            raise CADException(f"Error getting extents of block '{block_name}': {e}")

    # Add overall dimensions to an object or block
    def add_overall_dimensions(self, entity):
        try:
            min_point, max_point = APoint(*entity.GeometricExtents.MinPoint), APoint(*entity.GeometricExtents.MaxPoint)
            # Horizontal dimension
            self.add_dimension(Dimension(min_point, APoint(max_point.x, min_point.y, min_point.z),
                                         APoint(min_point.x, min_point.y - 5, min_point.z), DimensionType.ALIGNED))
            # Vertical dimension
            self.add_dimension(Dimension(min_point, APoint(min_point.x, max_point.y, min_point.z),
                                         APoint(min_point.x - 5, min_point.y, min_point.z), DimensionType.ALIGNED))
        except Exception as e:
            raise CADException(f"Error adding overall dimensions: {e}")

    # Get user-defined blocks in the document
    def get_user_defined_blocks(self):
        try:
            blocks = self.doc.Blocks
            user_defined_blocks = [block.Name for block in blocks
                                   if not block.IsLayout and not block.Name.startswith('*') and block.Name != 'GENAXEH']
            return user_defined_blocks
        except Exception as e:
            raise CADException(f"Error getting user-defined blocks: {e}")

    # Create a new layer
    def create_layer(self, layer):
        try:
            layers = self.doc.Layers
            new_layer = layers.Add(layer.name)
            new_layer.Color = layer.color.value
            return new_layer
        except Exception as e:
            raise CADException(f"Error creating layer '{layer.name}': {e}")

    # Set the active layer
    def set_active_layer(self, layer_name):
        try:
            self.doc.ActiveLayer = self.doc.Layers.Item(layer_name)
        except Exception as e:
            raise CADException(f"Error setting active layer '{layer_name}': {e}")

    # Insert a block into the model space
    def insert_block(self, block):
        try:
            block_ref = self.modelspace.InsertBlock(block.insertion_point.to_variant(), block.name, block.scale,
                                                    block.scale, block.scale, block.rotation)
            return block_ref
        except Exception as e:
            raise CADException(f"Error inserting block '{block.name}': {e}")

    # Save the document with a new name
    def save_as(self, file_path):
        try:
            self.doc.SaveAs(file_path)
        except Exception as e:
            raise CADException(f"Error saving document as '{file_path}': {e}")

    # Save the document
    def save(self):
        """Saves the active document."""
        try:
            self.doc.Save()
            print("Document saved successfully.")
        except Exception as e:
            raise CADException(f"Error saving AutoCAD document: {e}")

    # Close with save changes or not
    def close(self, save_changes=True):
        """Closes the active document.
        Args:
            save_changes (bool): If True, saves before closing. If False, closes without saving.
        """
        try:
            self.doc.Close(SaveChanges=save_changes)
            print("Document closed successfully.")
        except Exception as e:
            raise CADException(f"Error closing AutoCAD document: {e}")

    # Open an existing file
    def open_file(self, file_path):
        try:
            self.acad.Documents.Open(file_path)
        except Exception as e:
            raise CADException(f"Error opening file '{file_path}': {e}")

    # Get the insertion coordinates of a specific block
    def get_block_coordinates(self, block_name):
        try:
            block_references = []
            for entity in self.iter_objects("AcDbBlockReference"):
                if entity.Name == block_name:
                    insertion_point = entity.InsertionPoint
                    block_references.append(APoint(insertion_point[0], insertion_point[1], insertion_point[2]))
            return block_references
        except Exception as e:
            raise CADException(f"Error getting coordinates of block '{block_name}': {e}")

    # Delete an object
    def delete_object(self, obj):
        try:
            obj.Delete()
        except Exception as e:
            raise CADException(f"Error deleting object: {e}")

    # Clone an object
    def clone_object(self, obj, new_insertion_point):
        try:
            cloned_obj = obj.Copy(new_insertion_point.to_variant())
            return cloned_obj
        except Exception as e:
            raise CADException(f"Error cloning object: {e}")

    # Modify a property of an object
    def modify_object_property(self, obj, property_name, new_value):
        try:
            setattr(obj, property_name, new_value)
        except Exception as e:
            raise CADException(f"Error modifying property '{property_name}' of object: {e}")

    # Repeat a block horizontally until a specified length is reached
    def repeat_block_horizontally(self, block_name, total_length, block_length, insertion_point):
        try:
            x, y, z = insertion_point.x, insertion_point.y, insertion_point.z
            num_blocks = total_length // block_length

            for i in range(int(num_blocks)):
                new_insertion_point = APoint(x + i * block_length, y, z)
                self.insert_block(BlockReference(block_name, new_insertion_point))
        except Exception as e:
            raise CADException(f"Error repeating block '{block_name}' horizontally: {e}")

    # Set the visibility of a layer
    def set_layer_visibility(self, layer_name, visible=True):
        try:
            layer = self.doc.Layers.Item(layer_name)
            layer.LayerOn = visible
        except Exception as e:
            raise CADException(f"Error setting visibility of layer '{layer_name}': {e}")

    # Lock or unlock a layer
    def lock_layer(self, layer_name, lock=True):
        try:
            layer = self.doc.Layers.Item(layer_name)
            layer.Lock = lock
        except Exception as e:
            raise CADException(f"Error locking/unlocking layer '{layer_name}': {e}")

    # Delete a layer
    def delete_layer(self, layer_name):
        try:
            layer = self.doc.Layers.Item(layer_name)
            layer.Delete()
        except Exception as e:
            raise CADException(f"Error deleting layer '{layer_name}': {e}")

    # Change the color of a layer
    def change_layer_color(self, layer_name, color):
        try:
            layer = self.doc.Layers.Item(layer_name)
            layer.color = color.value
        except Exception as e:
            raise CADException(f"Error changing color of layer '{layer_name}': {e}")

    # Set the linetype of a layer
    def set_layer_linetype(self, layer_name, linetype_name):
        try:
            layer = self.doc.Layers.Item(layer_name)
            linetypes = self.doc.Linetypes
            if linetype_name not in linetypes:
                self.doc.Linetypes.Load(linetype_name, linetype_name)
            layer.Linetype = linetype_name
        except Exception as e:
            raise CADException(f"Error setting linetype of layer '{layer_name}': {e}")

    # Move an object
    def move_object(self, obj, new_insertion_point):
        try:
            obj.Move(obj.InsertionPoint, new_insertion_point.to_variant())
        except Exception as e:
            raise CADException(f"Error moving object: {e}")

    # Scale an object
    def scale_object(self, obj, base_point, scale_factor):
        try:
            obj.ScaleEntity(base_point.to_variant(), scale_factor)
        except Exception as e:
            raise CADException(f"Error scaling object: {e}")

    # Rotate an object
    def rotate_object(self, obj, base_point, rotation_angle):
        try:
            obj.Rotate(base_point.to_variant(), rotation_angle)
        except Exception as e:
            raise CADException(f"Error rotating object: {e}")

    # Align objects
    def align_objects(self, objects, alignment=Alignment.LEFT):
        try:
            if not objects:
                return
            if alignment == Alignment.LEFT:
                min_x = min(obj.InsertionPoint[0] for obj in objects)
                for obj in objects:
                    self.move_object(obj, APoint(min_x, obj.InsertionPoint[1], obj.InsertionPoint[2]))
            elif alignment == Alignment.RIGHT:
                max_x = max(obj.InsertionPoint[0] for obj in objects)
                for obj in objects:
                    self.move_object(obj, APoint(max_x, obj.InsertionPoint[1], obj.InsertionPoint[2]))
            elif alignment == Alignment.CENTER:
                center_x = (min(obj.InsertionPoint[0] for obj in objects) + max(
                    obj.InsertionPoint[0] for obj in objects)) / 2
                for obj in objects:
                    self.move_object(obj, APoint(center_x, obj.InsertionPoint[1], obj.InsertionPoint[2]))
        except Exception as e:
            raise CADException(f"Error aligning objects: {e}")

    # Distribute objects with specified spacing
    def distribute_objects(self, objects, spacing):
        try:
            if not objects:
                return
            objects.sort(key=lambda obj: obj.InsertionPoint[0])
            for i in range(1, len(objects)):
                new_x = objects[i - 1].InsertionPoint[0] + spacing
                self.move_object(objects[i], APoint(new_x, objects[i].InsertionPoint[1], objects[i].InsertionPoint[2]))
        except Exception as e:
            raise CADException(f"Error distributing objects: {e}")

    # Insert a block from a file
    def insert_block_from_file(self, file_path, insertion_point, scale=1.0, rotation=0.0):
        try:
            block_name = self.doc.Blocks.Import(file_path, file_path)
            block_ref = self.modelspace.InsertBlock(insertion_point.to_variant(), block_name, scale, scale, scale,
                                                    rotation)
            return block_ref
        except Exception as e:
            raise CADException(f"Error inserting block from file '{file_path}': {e}")

    # Export a block to a file
    def export_block_to_file(self, block_name, file_path):
        try:
            block = self.doc.Blocks.Item(block_name)
            block.Export(file_path)
        except Exception as e:
            raise CADException(f"Error exporting block '{block_name}' to '{file_path}': {e}")

    # Modify a block attribute
    def modify_block_attribute(self, block_ref, tag, new_value):
        try:
            for attribute in block_ref.GetAttributes():
                if attribute.TagString == tag:
                    attribute.TextString = new_value
        except Exception as e:
            raise CADException(f"Error modifying block attribute '{tag}': {e}")

    def modify_block_attribute_by_old_value(self, block_ref, tag, old_value, new_value):
        try:
            for attribute in block_ref.GetAttributes():
                if attribute.TagString == tag:
                    if attribute.TextString == old_value:
                        attribute.TextString = new_value
        except Exception as e:
            raise CADException(f"Error modifying block attribute '{tag}': {e}")

    # Delete a block attribute
    def delete_block_attribute(self, block_ref, tag):
        try:
            for attribute in block_ref.GetAttributes():
                if attribute.TagString == tag:
                    attribute.Delete()
        except Exception as e:
            raise CADException(f"Error deleting block attribute '{tag}': {e}")

    # Request point input from the user
    def get_user_input_point(self, prompt="Select a point"):
        try:
            point = self.doc.Utility.GetPoint(None, prompt)
            return APoint(point[0], point[1], point[2])
        except Exception as e:
            raise CADException(f"Error getting point input from user: {e}")

    # Request string input from the user
    def get_user_input_string(self, prompt="Enter a string"):
        try:
            return self.doc.Utility.GetString(False, prompt)
        except Exception as e:
            raise CADException(f"Error getting string input from user: {e}")

    # Request integer input from the user
    def get_user_input_integer(self, prompt="Enter an integer"):
        try:
            return self.doc.Utility.GetInteger(prompt)
        except Exception as e:
            raise CADException(f"Error getting integer input from user: {e}")

    # Display a message to the user
    def show_message(self, message):
        try:
            self.doc.Utility.Prompt(message + "\n")
        except Exception as e:
            raise CADException(f"Error displaying message: {e}")

    # Create a group of objects
    def create_group(self, group_name, objects):
        try:
            group = self.doc.Groups.Add(group_name)
            for obj in objects:
                group.AppendItems([obj])
            return group
        except Exception as e:
            raise CADException(f"Error creating group '{group_name}': {e}")

    # Add objects to a group
    def add_to_group(self, group_name, objects):
        try:
            group = self.doc.Groups.Item(group_name)
            for obj in objects:
                group.AppendItems([obj])
        except Exception as e:
            raise CADException(f"Error adding objects to group '{group_name}': {e}")

    # Remove objects from a group
    def remove_from_group(self, group_name, objects):
        try:
            group = self.doc.Groups.Item(group_name)
            for obj in objects:
                group.RemoveItems([obj])
        except Exception as e:
            raise CADException(f"Error removing objects from group '{group_name}': {e}")

    # Select a group of objects
    def select_group(self, group_name):
        try:
            group = self.doc.Groups.Item(group_name)
            return [item for item in group.GetItems()]
        except Exception as e:
            raise CADException(f"Error selecting group '{group_name}': {e}")
