# SPDX-License-Identifier: GPL-2.0-or-later

import bpy
import os
from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty
from .dxfimport.do import Do, Indicator
from .transverse_mercator import TransverseMercator


try:
    from pyproj import Proj, transform
    PYPROJ = True
except:
    PYPROJ = False

bl_info = {
    "name": "Import AutoCAD DXF Format (.dxf)",
    "author": "Lukas Treyer, Manfred Moitzi (support + dxfgrabber library), Vladimir Elistratov, Bastien Montagne, Remigiusz Fiedler (AKA migius)",
    "version": (0, 9, 6),
    "blender": (2, 80, 0),
    "location": "File > Import > AutoCAD DXF",
    "description": "Import files in the Autocad DXF format (.dxf)",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/scene_dxf.html",
    "category": "Import-Export",
}


proj_none_items = (
    ('NONE', "None", "No Coordinate System is available / will be set"),
)
proj_user_items = (
    ('USER', "User Defined", "Define the EPSG code"),
)
proj_tmerc_items = (
    ('TMERC', "Transverse Mercator", "Mercator Projection using a lat/lon coordinate as its geo-reference"),
)
proj_epsg_items = (
    ('EPSG:4326', "WGS84", "World Geodetic System 84; default for lat / lon; EPSG:4326"),
    ('EPSG:3857', "Spherical Mercator", "Webbrowser mapping service standard (Google, OpenStreetMap, ESRI); EPSG:3857"),
    ('EPSG:27700', "National Grid U.K",
                   "Ordnance Survey National Grid reference system used in Great Britain; EPSG:27700"),
    ('EPSG:2154', "France (Lambert 93)", "Lambert Projection for France; EPSG:2154"),
    ('EPSG:5514', "Czech Republic & Slovakia", "Coordinate System for Czech Republic and Slovakia; EPSG:5514"),
    ('EPSG:5243', "LLC Germany", "Projection for Germany; EPSG:5243"),
    ('EPSG:28992', "Amersfoort Netherlands", "Amersfoort / RD New -- Netherlands; EPSG:28992"),
    ('EPSG:21781', "Swiss CH1903 / LV03", "Switzerland and Lichtenstein; EPSG:21781"),
    ('EPSG:5880', "Brazil Polyconic", "Cartesian 2D; Central, South America; EPSG:5880 "),
    ('EPSG:42103', "LCC USA", "Lambert Conformal Conic Projection; EPSG:42103"),
    ('EPSG:3350', "Russia: Pulkovo 1942 / CS63 zone C0", "Russian Federation - onshore and offshore; EPSG:3350"),
    ('EPSG:22293', "Cape / Lo33 South Africa", "South Africa; EPSG:22293"),
    ('EPSG:27200', "NZGD49 / New Zealand Map Grid", "NZGD49 / New Zealand Map Grid; EPSG:27200"),
    ('EPSG:3112', "GDA94 Australia Lambert", "GDA94 / Geoscience Australia Lambert; EPSG:3112"),
    ('EPSG:24378', "India zone I", "Kalianpur 1975 / India zone I; EPSG:24378"),
    ('EPSG:2326', "Hong Kong 1980 Grid System", "Hong Kong 1980 Grid System; EPSG:2326"),
    ('EPSG:3414', "SVY21 / Singapore TM", "SVY21 / Singapore TM; EPSG:3414"),
)

proj_epsg_dict = {e[0]: e[1] for e in proj_epsg_items}

__version__ = '.'.join([str(s) for s in bl_info['version']])

BY_LAYER = 0
BY_DXFTYPE = 1
BY_CLOSED_NO_BULGE_POLY = 2
SEPARATED = 3
LINKED_OBJECTS = 4
GROUP_INSTANCES = 5
BY_BLOCKS = 6

merge_map = {"BY_LAYER": BY_LAYER, "BY_TYPE": BY_DXFTYPE,
             "BY_CLOSED_NO_BULGE_POLY": BY_CLOSED_NO_BULGE_POLY, "BY_BLOCKS": BY_BLOCKS}

T_Merge = True
T_ImportText = True
T_ImportLight = True
T_ExportAcis = False
T_MergeLines = True
T_OutlinerGroups = True
T_Bbox = True
T_CreateNewScene = False
T_Recenter = False
T_ThicknessBevel = True
T_import_atts = True

RELEASE_TEST = False
DEBUG = False


def is_ref_scene(scene):
    return "latitude" in scene and "longitude" in scene


def read(report, filename, obj_merge=BY_LAYER, import_text=True, import_light=True, export_acis=True, merge_lines=True,
         do_bbox=True, block_rep=LINKED_OBJECTS, new_scene=None, recenter=False, projDXF=None, projSCN=None,
         thicknessWidth=True, but_group_by_att=True, dxf_unit_scale=1.0):
    # import dxf and export nurbs types to sat/sab files
    # because that's how autocad stores nurbs types in a dxf...
    do = Do(filename, obj_merge, import_text, import_light, export_acis, merge_lines, do_bbox, block_rep, recenter,
            projDXF, projSCN, thicknessWidth, but_group_by_att, dxf_unit_scale)

    errors = do.entities(os.path.basename(filename).replace(".dxf", ""), new_scene)

    # display errors
    for error in errors:
        report({'ERROR', 'INFO'}, error)

    # inform the user about the sat/sab files
    if len(do.acis_files) > 0:
        report({'INFO'}, "Exported %d NURBS objects to sat/sab files next to your DXF file" % len(do.acis_files))


def display_groups_in_outliner():
    outliners = (a for a in bpy.context.screen.areas if a.type == "OUTLINER")
    for outliner in outliners:
        pass
        #outliner.spaces[0].display_mode = "GROUPS"


# Update helpers (must be globals to be re-usable).
def _update_use_georeferencing_do(self, context):
    if not self.create_new_scene:
        scene = context.scene
        # Try to get Scene SRID (ESPG) data from current scene.
        srid = scene.get("SRID", None)
        if srid is not None:
            self.internal_using_scene_srid = True
            srid = srid.upper()
            if srid == 'TMERC':
                self.proj_scene = 'TMERC'
                self.merc_scene_lat = scene.get('latitude', 0)
                self.merc_scene_lon = scene.get('longitude', 0)
            else:
                if srid in (p[0] for p in proj_epsg_items):
                    self.proj_scene = srid
                else:
                    self.proj_scene = 'USER'
                    self.epsg_scene_user = srid
        else:
            self.internal_using_scene_srid = False
    else:
        self.internal_using_scene_srid = False


def _recenter_allowed(self):
    scene = bpy.context.scene
    conditional_requirement = self.proj_scene == 'TMERC' if PYPROJ else self.dxf_indi == "SPHERICAL"
    return not (
                    self.use_georeferencing and
                    (
                        conditional_requirement or
                        (not self.create_new_scene and is_ref_scene(scene))
                    )
                    )


def _set_recenter(self, value):
    self.recenter = value if _recenter_allowed(self) else False


def _update_proj_scene_do(self, context):
    # make sure scene EPSG is not None if DXF EPSG is not None
    if self.proj_scene == 'NONE' and self.proj_dxf != 'NONE':
        self.proj_scene = self.proj_dxf


def _update_import_atts_do(self, context):
    mo = merge_map[self.merge_options]
    if mo == BY_CLOSED_NO_BULGE_POLY or mo == BY_BLOCKS:
        self.import_atts = False
        self.represent_thickness_and_width = False
    elif self.represent_thickness_and_width and self.merge:
        self.import_atts = True
    elif not self.merge:
        self.import_atts = False


class IMPORT_OT_dxf(bpy.types.Operator):
    """Import from DXF file format (.dxf)"""
    bl_idname = "import_scene.dxf"
    bl_description = 'Import from DXF file format (.dxf)'
    bl_label = "Import DXf v." + __version__
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_options = {'UNDO'}

    filepath: StringProperty(
            name="input file",
            subtype='FILE_PATH'
            )

    filename_ext = ".dxf"

    filter_glob: StringProperty(
            default="*.dxf",
            options={'HIDDEN'},
            )

    def _update_merge(self, context):
        _update_import_atts_do(self, context)
    merge: BoolProperty(
            name="Merged Objects",
            description="Merge DXF entities to Blender objects",
            default=T_Merge,
            update=_update_merge
            )

    def _update_merge_options(self, context):
        _update_import_atts_do(self, context)

    merge_options: EnumProperty(
            name="Merge",
            description="Merge multiple DXF entities into one Blender object",
            items=[('BY_LAYER', "By Layer", "Merge DXF entities of a layer to an object"),
                   ('BY_TYPE', "By Layer AND DXF-Type", "Merge DXF entities by type AND layer"),
                   ('BY_CLOSED_NO_BULGE_POLY', "By Layer AND closed no-bulge polys", "Polys can have a transformation attribute that makes DXF polys resemble Blender mesh faces quite a bit. Merging them results in one MESH object."),
                   ('BY_BLOCKS', "By Layer AND DXF-Type AND Blocks", "Merging blocks results in all uniformly scaled blocks being referenced by a dupliface mesh instead of object containers. Non-uniformly scaled blocks will be imported as indicated by 'Blocks As'.")],
            default='BY_LAYER',
            update=_update_merge_options
            )

    merge_lines: BoolProperty(
            name="Combine LINE entities to polygons",
            description="Checks if lines are connect on start or end and merges them to a polygon",
            default=T_MergeLines
            )

    import_text: BoolProperty(
            name="Import Text",
            description="Import DXF Text Entities MTEXT and TEXT",
            default=T_ImportText,
            )

    import_light: BoolProperty(
            name="Import Lights",
            description="Import DXF Text Entity LIGHT",
            default=T_ImportLight
            )

    export_acis: BoolProperty(
            name="Export ACIS Entities",
            description="Export Entities consisting of ACIS code to ACIS .sat/.sab files",
            default=T_ExportAcis
            )

    outliner_groups: BoolProperty(
            name="Display Groups in Outliner(s)",
            description="Make all outliners in current screen layout show groups",
            default=T_OutlinerGroups
            )

    do_bbox: BoolProperty(
            name="Parent Blocks to Bounding Boxes",
            description="Create a bounding box for blocks with more than one object (faster without)",
            default=T_Bbox
            )



    block_options: EnumProperty(
            name="Blocks As",
            description="Select the representation of DXF blocks: linked objects or group instances",
            items=[('LINKED_OBJECTS', "Linked Objects", "Block objects get imported as linked objects"),
                   ('GROUP_INSTANCES', "Group Instances", "Block objects get imported as group instances")],
            default='LINKED_OBJECTS',

            )

    def _update_create_new_scene(self, context):
        _update_use_georeferencing_do(self, context)
        _set_recenter(self, self.recenter)
    create_new_scene: BoolProperty(
            name="Import DXF to new scene",
            description="Creates a new scene with the name of the imported file",
            default=T_CreateNewScene,
            update=_update_create_new_scene,
            )

    recenter: BoolProperty(
            name="Center geometry to scene",
            description="Moves geometry to the center of the scene",
            default=T_Recenter,
            )

    def _update_thickness_width(self, context):
        _update_import_atts_do(self, context)
    represent_thickness_and_width: BoolProperty(
            name="Represent line thickness/width",
            description="Map thickness and width of lines to Bevel objects and extrusion attribute",
            default=T_ThicknessBevel,
            update=_update_thickness_width
            )

    import_atts: BoolProperty(
            name="Merge by attributes",
            description="If 'Merge objects' is on but thickness and width are not chosen to be represented, with this "
                        "option object still can be merged by thickness, with, subd and extrusion attributes "
                        "(extrusion = transformation matrix of DXF objects)",
            default=T_import_atts
            )

    # geo referencing

    def _update_use_georeferencing(self, context):
        _update_use_georeferencing_do(self, context)
        _set_recenter(self, self.recenter)
    use_georeferencing: BoolProperty(
            name="Geo Referencing",
            description="Project coordinates to a given coordinate system or reference point",
            default=True,
            update=_update_use_georeferencing,
            )

    def _update_dxf_indi(self, context):
        _set_recenter(self, self.recenter)
    dxf_indi: EnumProperty(
            name="DXF coordinate type",
            description="Indication for spherical or euclidean coordinates",
            items=[('EUCLIDEAN', "Euclidean", "Coordinates in x/y"),
                   ('SPHERICAL', "Spherical", "Coordinates in lat/lon")],
            default='EUCLIDEAN',
            update=_update_dxf_indi,
            )

    # Note: FloatProperty is not precise enough, e.g. 1.0 becomes 0.999999999. Python is more precise here (it uses
    #       doubles internally), so we store it as string here and convert to number with py's float() func.
    dxf_scale: StringProperty(
            name="Unit Scale",
            description="Coordinates are assumed to be in meters; deviation must be indicated here",
            default="1.0"
            )

    def _update_proj(self, context):
        _update_proj_scene_do(self, context)
        _set_recenter(self, self.recenter)
    if PYPROJ:
        pitems = proj_none_items + proj_user_items + proj_epsg_items
        proj_dxf: EnumProperty(
            name="DXF SRID",
            description="The coordinate system for the DXF file (check http://epsg.io)",
            items=pitems,
            default='NONE',
            update=_update_proj,
            )

    epsg_dxf_user: StringProperty(name="EPSG-Code", default="EPSG")
    merc_dxf_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
    merc_dxf_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)

    pitems = proj_none_items + ((proj_user_items + proj_tmerc_items + proj_epsg_items) if PYPROJ else proj_tmerc_items)
    proj_scene: EnumProperty(
            name="Scn SRID",
            description="The coordinate system for the Scene (check http://epsg.io)",
            items=pitems,
            default='NONE',
            update=_update_proj,
            )

    epsg_scene_user: StringProperty(name="EPSG-Code", default="EPSG")
    merc_scene_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
    merc_scene_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)

    # internal use only!
    internal_using_scene_srid: BoolProperty(default=False, options={'HIDDEN'})

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        # merge options
        layout.label(text="Merge Options:")
        box = layout.box()
        sub = box.row()
        #sub.enabled = merge_map[self.merge_options] != BY_BLOCKS
        sub.prop(self, "block_options")
        box.prop(self, "do_bbox")
        box.prop(self, "merge")
        sub = box.row()
        sub.enabled = self.merge
        sub.prop(self, "merge_options")
        box.prop(self, "merge_lines")

        # general options
        layout.label(text="Line thickness and width:")
        box = layout.box()
        box.enabled = not merge_map[self.merge_options] == BY_CLOSED_NO_BULGE_POLY
        box.prop(self, "represent_thickness_and_width")
        sub = box.row()
        sub.enabled = (not self.represent_thickness_and_width and self.merge)
        sub.prop(self, "import_atts")

        # optional objects
        layout.label(text="Optional Objects:")
        box = layout.box()
        box.prop(self, "import_text")
        box.prop(self, "import_light")
        box.prop(self, "export_acis")

        # view options
        layout.label(text="View Options:")
        box = layout.box()
        box.prop(self, "outliner_groups")
        box.prop(self, "create_new_scene")
        sub = box.row()
        sub.enabled = _recenter_allowed(self)
        sub.prop(self, "recenter")

        # geo referencing
        layout.prop(self, "use_georeferencing", text="Geo Referencing:")
        box = layout.box()
        box.enabled = self.use_georeferencing
        self.draw_pyproj(box, context.scene) if PYPROJ else self.draw_merc(box)

    def draw_merc(self, box):
        box.label(text="DXF File:")
        box.prop(self, "dxf_indi")
        box.prop(self, "dxf_scale")

        sub = box.column()
        sub.enabled = not _recenter_allowed(self)
        sub.label(text="Geo Reference:")
        sub = box.column()
        sub.enabled = not _recenter_allowed(self)
        if is_ref_scene(bpy.context.scene):
            sub.enabled = False
        sub.prop(self, "merc_scene_lat", text="Lat")
        sub.prop(self, "merc_scene_lon", text="Lon")

    def draw_pyproj(self, box, scene):
        valid_dxf_srid = True

        # DXF SCALE
        box.prop(self, "dxf_scale")

        # EPSG DXF
        box.alert = (self.proj_scene != 'NONE' and (not valid_dxf_srid or self.proj_dxf == 'NONE'))
        box.prop(self, "proj_dxf")
        box.alert = False
        if self.proj_dxf == 'USER':
            try:
                Proj(init=self.epsg_dxf_user)
            except:
                box.alert = True
                valid_dxf_srid = False
            box.prop(self, "epsg_dxf_user")
        box.alert = False

        box.separator()

        # EPSG SCENE
        col = box.column()
        # Only info in case of pre-defined EPSG from current scene.
        if self.internal_using_scene_srid:
            col.enabled = False

        col.prop(self, "proj_scene")

        if self.proj_scene == 'USER':
            try:
                Proj(init=self.epsg_scene_user)
            except Exception as e:
                col.alert = True
            col.prop(self, "epsg_scene_user")
            col.alert = False
            col.label(text="")  # Placeholder.
        elif self.proj_scene == 'TMERC':
            col.prop(self, "merc_scene_lat", text="Lat")
            col.prop(self, "merc_scene_lon", text="Lon")
        else:
            col.label(text="")  # Placeholder.
            col.label(text="")  # Placeholder.

        # user info
        if self.proj_scene != 'NONE':
            if not valid_dxf_srid:
                box.label(text="DXF SRID not valid", icon="ERROR")
            if self.proj_dxf == 'NONE':
                box.label(text="", icon='ERROR')
                box.label(text="DXF SRID must be set, otherwise")
                if self.proj_scene == 'USER':
                    code = self.epsg_scene_user
                else:
                    code = self.proj_scene
                box.label(text='Scene SRID %r is ignored!' % code)

    def execute(self, context):
        block_map = {"LINKED_OBJECTS": LINKED_OBJECTS, "GROUP_INSTANCES": GROUP_INSTANCES}
        merge_options = SEPARATED
        if self.merge:
            merge_options = merge_map[self.merge_options]
        scene = bpy.context.scene
        if self.create_new_scene:
            scene = bpy.data.scenes.new(os.path.basename(self.filepath).replace(".dxf", ""))

        proj_dxf = None
        proj_scn = None
        dxf_unit_scale = 1.0
        if self.use_georeferencing:
            dxf_unit_scale = float(self.dxf_scale.replace(",", "."))
            if PYPROJ:
                if self.proj_dxf != 'NONE':
                    if self.proj_dxf == 'USER':
                        proj_dxf = Proj(init=self.epsg_dxf_user)
                    else:
                        proj_dxf = Proj(init=self.proj_dxf)
                if self.proj_scene != 'NONE':
                    if self.proj_scene == 'USER':
                        proj_scn = Proj(init=self.epsg_scene_user)
                    elif self.proj_scene == 'TMERC':
                        proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
                    else:
                        proj_scn = Proj(init=self.proj_scene)
            else:
                proj_dxf = Indicator(self.dxf_indi)
                proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)

        if RELEASE_TEST:
            # for release testing
            from . import test
            test.test()
        else:
            read(self.report, self.filepath, merge_options, self.import_text, self.import_light, self.export_acis,
                 self.merge_lines, self.do_bbox, block_map[self.block_options], scene, self.recenter,
                 proj_dxf, proj_scn, self.represent_thickness_and_width, self.import_atts, dxf_unit_scale)

        if self.outliner_groups:
            display_groups_in_outliner()

        return {'FINISHED'}

    def invoke(self, context, event):
        # Force first update...
        self._update_use_georeferencing(context)

        wm = context.window_manager
        wm.fileselect_add(self)
        return {'RUNNING_MODAL'}


def menu_func(self, context):
    self.layout.operator(IMPORT_OT_dxf.bl_idname, text="AutoCAD DXF")


def register():
    bpy.utils.register_class(IMPORT_OT_dxf)
    bpy.types.TOPBAR_MT_file_import.append(menu_func)


def unregister():
    bpy.utils.unregister_class(IMPORT_OT_dxf)
    bpy.types.TOPBAR_MT_file_import.remove(menu_func)


if __name__ == "__main__":
    register()
