from encodings import utf_8
import bpy
import json

# import bpy_extras
from bpy_extras.object_utils import world_to_camera_view # as cam_space
from bpy_extras.io_utils import ExportHelper
from mathutils import Vector
from pathlib import Path
from . import fn

from bpy.props import (
    StringProperty,
    BoolProperty,
    FloatProperty,
    EnumProperty,
    CollectionProperty,
)

from bpy.app.translations import pgettext_data as data_

'''
def Export_AE_2d_position_json_data():
    scn = bpy.context.scene
    cam = scn.objects.get('anim_cam')
    if not cam:
        print('Active camera not "anim_cam"')
        cam = scn.camera

    rx = scn.render
    rx, ry = rd.resolution_x, rd.resolution_y

    targets = [o for o in bpy.context.selected_objects if o.type != 'CAMERA']

    for ob in targets:
        # get an idea of the scale relative to image res ? (too complex since it's not even the right resolution...)
        pos = world_to_camera_view(scn, scn.objects['anim_cam'], ob.matrix_world.to_translation())[:-1]
        pix_pos = Vector((pos[0]*rx, (1-pos[1])*ry))
'''

def export_ae_transforms(directory, selection=None, camera=None, exposition=True, prefix='ae_', suffix='', fr=False, export_format='txt', export_cam=True):
    """
    Export After Effects transform data for selected objects and camera in Blender.

    This function exports keyframe data for position, scale, and optional exposition for
    selected objects and the camera. The data can be exported in TXT and/or JSON formats,
    which can be imported into Adobe After Effects.

    Parameters:
    directory (str): The directory path where the exported files will be saved.
    selection (list, optional): List of Blender objects or pose bones to export. Defaults to None.
    camera (bpy.types.Object, optional): The camera object to use for calculations. Defaults to the scene's active camera.
    exposition (bool, optional): Whether to export exposition data. Defaults to True.
    prefix (str, optional): Prefix for the exported filenames. Defaults to 'ae_'.
    suffix (str, optional): Suffix for the exported filenames. Defaults to ''.
    fr (bool, optional): If True, uses French labels for exposition data. Defaults to False.
    export_format (str, optional): The format(s) to export. Can be 'txt', 'json', or 'txt,json'. Defaults to 'txt'.

    Example usage:
    export_ae_transforms('/path/to/export', selection=bpy.context.selected_objects, export_format='txt,json')
    """

    scn = bpy.context.scene
    res = Vector((scn.render.resolution_x, scn.render.resolution_y))

    def get_keyframe_data(coords_2d, exposition=False):
        frame_start, org_2d, _ = coords_2d[0]
        org_distance = org_2d[-1]
        
        keyframe_data = {'position': [], 'scale': []}

        for frame, co_2d, is_animated in coords_2d:
            position = Vector((co_2d[0], 1-co_2d[1]))
            position *= res # Multiply by resolution to get pixels value
            keyframe_data['position'].append((frame-frame_start, (round(position[0]), round(position[1]))))
                                    
            scale = (org_distance / co_2d[-1]) * 100            
            keyframe_data['scale'].append((frame-frame_start, (round(scale, 5),)*3))
            
            if is_animated:
                keyframe_data.setdefault('exposition', []).append(frame-frame_start)

        return keyframe_data

    def get_keyframe_data_txt(keyframe_data, exposition=True):
        keyframe_data_txt = (
            'Adobe After Effects 8.0 Keyframe Data\n\n'
            f'\tUnits Per Second\t{scn.render.fps}\n'
            f'\tSource Width\t{scn.render.resolution_x}\n'
            f'\tSource Height\t{scn.render.resolution_y}\n'
            '\tSource Pixel Aspect Ratio\t1\n'
            '\tComp Pixel Aspect Ratio\t1\n\n'
        )

        # Position
        keyframe_data_txt += (    
            '\nTransform\tPosition\n'
            '\tFrame\tX pixels\tY pixels\tZ pixels\t\n'
        )
        for frame, position in keyframe_data['position']:
            keyframe_data_txt += f'\t{frame}\t{position[0]}\t{position[1]}\t0\t\n'

        # Scale
        keyframe_data_txt += (
            '\nTransform\tScale\n'
            '\tFrame\tX percent\tY percent\tZ percent\t\n'
        )
        for frame, scale in keyframe_data['scale']:
            keyframe_data_txt += f'\t{frame}\t{scale[0]}\t{scale[1]}\t{scale[2]}\t\n'

        if exposition:
            if fr:
                keyframe_data_txt += '\nEffects\tParamètre case #1\tCase #2\t\n'
            else:
                keyframe_data_txt += '\nEffects\tCheckbox Control #1\tCheckbox #2\t\n'
            
            keyframe_data_txt += '\tFrame\t\t\n'

            for i, frame in enumerate(keyframe_data['exposition']):
                keyframe_data_txt += f'\t{frame}\t{i%2}\t\n'

        keyframe_data_txt += '\nEnd of Keyframe Data\n' 
        
        return keyframe_data_txt


    if selection is None:
        selection = []

    if camera is None:
        camera = scn.camera
    
    use_simplify = scn.render.use_simplify
    simplify_subdivision = scn.render.simplify_subdivision
    scn.render.use_simplify = True
    scn.render.simplify_subdivision = 0
    
    # Get the camera at the first frame for reference
    scn.frame_set(scn.frame_start)
    cam_org_3d = camera.matrix_world @ Vector((0, 0, -100))
        
    camera_coords = []
    ob_coords = {}
    animation = {}
    for i in range(scn.frame_start, scn.frame_end + 1):
        scn.frame_set(i)
        
        if camera is not None:
            co_2d = world_to_camera_view(scn, camera, cam_org_3d)
            camera_coords.append((i, co_2d, False))
            
        for item in selection:
            if isinstance(item, bpy.types.PoseBone):
                co_3d = item.id_data.matrix_world @ item.matrix.to_translation()
                name = f'{item.id_data.name}_{item.name}'
                rig_transforms = [tuple(round(x, 5) for v in b.matrix for x in v) for b in item.id_data.pose.bones]
                is_animated = rig_transforms != animation.get(name)
                animation[name] = rig_transforms
            elif item.type == 'GPENCIL': # isinstance(item.data, bpy.types.GreasePencil):
                name = item.name
                co_3d = item.matrix_world.to_translation()
                ## Check if there is a GP-scene at scene-frame (i) on any visible layer
                is_animated = i in [f.frame_number for l in item.data.layers for f in l.frames if not l.hide]
                animation[name] = co_3d
            else:
                name = item.name
                co_3d = item.matrix_world.to_translation()
                is_animated = co_3d != animation.get(name)
                animation[name] = co_3d

            co_2d = world_to_camera_view(scn, camera, co_3d)
            ob_coords.setdefault(name, []).append((i, co_2d, is_animated))
    
    cam_key_frame_data = get_keyframe_data(camera_coords)
    objs_keyframe_data = {name: get_keyframe_data(co, exposition=exposition) for name, co in ob_coords.items()} 

    if 'json' in export_format or export_format == 'json':
        keyframe_data = {
                "units_per_second": scn.render.fps,
                "source_width": scn.render.resolution_x,
                "source_height": scn.render.resolution_y,
                "source_pixel_aspect_ratio": 1,
                "comp_pixel_aspect_ratio": 1,
                'objects': objs_keyframe_data
            }
        if export_cam: #camera is not None:
            keyframe_data['camera'] = cam_key_frame_data
                
        Path(directory, f'{prefix}keyframe_data{suffix}.json').write_text(json.dumps(keyframe_data, indent=4), encoding='utf-8')
    
    if 'txt' in export_format or export_format == 'txt':
        for name, keyframe_data in objs_keyframe_data.items():
            name = name.replace('.', '_')
            keyframe_data_txt = get_keyframe_data_txt(keyframe_data, exposition=exposition)
            Path(directory, f'{prefix}{name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
        
        if export_cam: #camera is not None:
            keyframe_data_txt = get_keyframe_data_txt(cam_key_frame_data, exposition=False)
            Path(directory, f'{prefix}{camera.name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
    
    scn.render.use_simplify = use_simplify
    scn.render.simplify_subdivision = simplify_subdivision


## Export operator without export_helper
class GPEXP_OT_export_anim_to_ae(bpy.types.Operator):
    bl_idname = "gp.export_anim_to_ae"
    bl_label = "Export AE Files"
    bl_description = "Export the animation to After Effects, 2D transform of objects, camera\
        \nand/or exposition (including greasepencil frames)"
    bl_options = {"REGISTER"}

    # filter_glob: StringProperty(default='*.*', options={'HIDDEN'})# *.jpeg;*.png;*.tif;*.tiff;*.bmp

    # filename_ext = ''

    # filepath : StringProperty(
    #     name="File Path",
    #     description="File path used for export", 
    #     maxlen= 1024)

    ## Only need directory
    directory : StringProperty(
        name="File Path",
        description="File path used for export", 
        maxlen= 1024,
        subtype='DIR_PATH'
    )
    prefix : StringProperty(
        name="Prefix",
        default='ae_',
        description="Prefix name for exported txt and json files", 
        maxlen= 1024,
    )

    use_selection: BoolProperty(
        name="Selected Objects",
        description="Export selected and visible objects only",
        default=True,
    )
    use_visible: BoolProperty(
        name='Visible Objects',
        description='Export visible objects only',
        default=False
    )
    use_active_collection: BoolProperty(
        name="Active Collection",
        description="Export only objects from the active collection (and its children)",
        default=False,
    )
    object_types: EnumProperty(
        name="Object Types",
        options={'ENUM_FLAG'},
        items=(('EMPTY', "Empty", ""),
               ('CAMERA', "Camera", ""),
               ('LIGHT', "Lamp", ""),
               ('ARMATURE', "Armature", "WARNING: not supported in dupli/group instances"),
               ('MESH', "Mesh", ""),
               ('GPENCIL', "Grease Pencil", ""),
               ('OTHER', "Other", "Other geometry types, like curve, metaball, etc. (converted to meshes)"),
               ),
        description="Which kind of object to export",
        default={'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'},
    )
    exposition: BoolProperty(
        name='Exposition',
        description='Export the exposition of the keys',
        default=True
    )
    # use_object_keys: BoolProperty(
    #     name='Object Keys',
    #     description='Consider object transform keys for animated exposition',
    #     default=True
    # )
    use_active_camera: BoolProperty(
        name='Active Camera',
        description='Export active camera keys',
        default=True
    )
    data_lang: EnumProperty(
        name="AE Language",
        items=(('FR', "French", ""),
               ('EN', "English", ""),
               ),
        description="Clipboard keyframe data language",
        default='FR',
    )
    file_format: EnumProperty(
        name="File Type",
        options={'ENUM_FLAG'},
        items=(('txt', "txt", ""),
               ('json', "json", ""),
               ),
        description="File format to export (possible to select multiple choices with Shift + Click)",
        default={'txt'},
    )

    def invoke(self, context, _event):
        if not self.directory:
            blend_filepath = context.blend_data.filepath
            if blend_filepath:
                dest_folder = Path(blend_filepath).parent
                
                ## If pre-enter a specific subfolder exists
                ## (Commented, should be used with a project environment variable)
                # output_folder = dest_folder / 'render'
                # if output_folder.exists() and output_folder.is_dir():
                #     dest_folder = output_folder

                self.directory = str(dest_folder)
                
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        layout.use_property_decorate = False  # No animation.

        # Are we inside the File browser
        is_file_browser = context.space_data.type == 'FILE_BROWSER'

        export_main(layout, self, is_file_browser)
        export_panel_format(layout, self, is_file_browser)
        export_panel_include(layout, self, is_file_browser)

    def execute(self, context):
        objects_selection = get_object_selection(use_selection=self.use_selection,
                             use_visible=self.use_visible,
                             use_active_collection=self.use_active_collection,
                             object_types=self.object_types
                             )

        ## Ensure output path is directory
        output_path = Path(self.directory)
        if not output_path.is_dir():
            output_path = output_path.parent

        print('Output directory: ', output_path)
        
        cam = None
        if context.scene.camera:
            cam = context.scene.camera
        
        if cam and cam in objects_selection:
            ## Remove active camera from objects list
            objects_selection.pop(objects_selection.index(cam))

        print('Export AE transform from objects:')
        for o in objects_selection:
            print('- ', o.name)

        export_ae_transforms(directory=output_path,
                             selection=objects_selection,
                             camera=cam,
                             exposition=self.exposition,
                             prefix=self.prefix,
                             fr=self.data_lang == 'FR',
                             export_format=self.file_format,
                             export_cam=self.use_active_camera)

        self.report({'INFO'}, f'File(s) saved in folder: {output_path}')
        return {"FINISHED"}


def get_object_selection(use_selection=False, use_visible=False, use_active_collection=False, object_types=None):
    context = bpy.context
    ## determine selection based on filters4
    source_collection = None
    if use_active_collection:
        source_collection = context.view_layer.active_layer_collection.collection

    if source_collection:
        if use_selection:
            ctx_objects = tuple(obj for obj in source_collection.all_objects if obj.select_get())
        else:
            ctx_objects = source_collection.all_objects
    else:
        if use_selection:
            ctx_objects = context.selected_objects
        else:
            ctx_objects = context.view_layer.objects
    
    if use_visible:
        ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get())

    ## Filter by object type
    if object_types is None:
        object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'}

    ctx_objects = [obj for obj in ctx_objects if obj.type in object_types]
    ## Constant for compatible other object type
    # BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}

    if 'OTHER' in object_types:
        ## Any object that is not in proposed list
        ctx_objects += [obj for obj in ctx_objects if obj.type not in object_types]
    
    return ctx_objects


def export_main(layout, operator, is_file_browser):
    col = layout.column()
    col.prop(operator, 'exposition')
    col.prop(operator, 'use_active_camera')
    # col.prop(operator, 'use_object_keys')

def export_panel_format(layout, operator, is_file_browser):
    header, body = layout.panel("AE_export_format", default_closed=False)
    header.label(text="Format")
    if body:
        col = body.column()
        col.prop(operator, 'prefix')
        ## Format (language and file)

        col.row().prop(operator, 'data_lang', expand=True)
        if is_file_browser:
            col.column().prop(operator, 'file_format')

def export_panel_include(layout, operator, is_file_browser):
    header, body = layout.panel("AE_export_include", default_closed=False)
    header.label(text="Include")
    if body:
        sublayout = body.column(heading="Limit to")
        # if is_file_browser:
        sublayout.prop(operator, "use_selection")
        sublayout.prop(operator, "use_visible")
        sublayout.prop(operator, "use_active_collection")

        body.column().prop(operator, "object_types")
        # body.prop(operator, "use_custom_props")

### --- Old functions

# Unused (old func that might not be usefull at all...)
def correct_shift(vec, cam):
    resX = bpy.context.scene.render.resolution_x
    resY = bpy.context.scene.render.resolution_y
    ratio = resX/resY
    shiftX = 2*cam.data.shift_x
    shiftY = 2*cam.data.shift_y

    if ratio<1:
        return vec - Vector((shiftX*(1/ratio), shiftY, 0))
    elif ratio>1:
        return vec - Vector((shiftX, shiftY*ratio, 0))
    else:
        return vec - Vector((shiftX, shiftY, 0))

def export_AE_objects_position_keys():
    '''Export keys as paperclip to paste in after'''

    scn = bpy.context.scene
    result = {}
    print(f'Exporting 2d position (scene range: {scn.frame_start} - {scn.frame_end})')
    for fr in range(scn.frame_start,scn.frame_end + 1):

        print(f'frame: {fr}')
        scn.frame_set(fr)

        for o in bpy.context.selected_objects:
            if not result.get(o.name):
                result[o.name] = []
            proj2d = world_to_camera_view(scn, scn.camera, o.matrix_world.to_translation()) # + Vector((.5,.5,0))

            # proj2d = correct_shift(proj2d, scn.camera) # needed ?
            x = (proj2d[0]) * scn.render.resolution_x
            y = -(proj2d[1]) * scn.render.resolution_y + scn.render.resolution_y

            result[o.name].append((fr,x,y))

    for name,value in result.items():

        txt = fn.get_ae_keyframe_clipboard_header(scn)

        for v in value:
            txt += '\t%s\t%s\t%s\t0\t\n'%(v[0],v[1],v[2]) # add 0 for Z (probably not needed)

        txt += '\n\nEnd of Keyframe Data\n' # keyframe txt footer

        blend = Path(bpy.data.filepath)
        keyfile = blend.parent / 'render' / f'pos_{name}.txt'
        keyfile.parent.mkdir(parents=False, exist_ok=True)

        print(f'exporting keys for {name} at {keyfile}')

        ## save forcing CRLF terminator (DOS style, damn windows)
        ## in case it's exported from linux
        with open(keyfile, 'w', newline='\r\n') as fd:
            fd.write(txt)


class GPEXP_OT_export_keys_to_ae(bpy.types.Operator):
    bl_idname = "gp.export_keys_to_ae"
    bl_label = "Export 2D Position To AE"
    bl_description = "Export selected objects positions as text file containing key paperclip for AfterEffects layers"
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context):
        return context.selected_objects

    def execute(self, context):
        export_AE_objects_position_keys()
        return {"FINISHED"}


def export_anim_cam_position(camera=None, context=None):
    context = context or bpy.context
    scn = context.scene

    camera = camera or bpy.data.objects.get('anim_cam')
    if not camera:
        return 'Abort: No "anim_cam" found!'

    text = fn.get_ae_keyframe_clipboard_header(scn)
    for i in range(scn.frame_start, scn.frame_end + 1):
        scn.frame_set(i)
        center = fn.get_cam_frame_center_world(camera)
        coord = fn.get_coord_in_cam_space(scn, scn.camera, center, ae=True)
        text += f'\t{i}\t{coord[0]}\t{coord[1]}\t\n'

    text += '\n\nEnd of Keyframe Data\n' # Ae Frame ending

    blend = Path(bpy.data.filepath)
    keyfile = blend.parent / 'render' / f'anim_cam_pos.txt'
    keyfile.parent.mkdir(parents=False, exist_ok=True)

    print(f'Exporting anim cam positions keys at: {keyfile}')
    with open(keyfile, 'w') as fd:
        fd.write(text)

class GPEXP_OT_export_cam_keys_to_ae(bpy.types.Operator):
    bl_idname = "gp.export_cam_keys_to_ae"
    bl_label = "Export Camera 2D Position To AE"
    bl_description = "Export anim cam positions as text file containing key paperclip for AfterEffects layers"
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context):
        return context.selected_objects

    def execute(self, context):
        cam = None
        if context.object and context.object.type == 'CAMERA' and context.object != context.scene.camera:
            cam = context.object

        err = export_anim_cam_position(camera=cam, context=context)
        if err:
            self.report({'ERROR'}, err)
            return {"CANCELLED"}
        return {"FINISHED"}

class GPEXP_OT_fix_overscan_shift(bpy.types.Operator):
    bl_idname = "gp.fix_overscan_shift"
    bl_label = "Fix Cam Shift Value With Overscan"
    bl_description = "(Gp render operator) change shift values to re-center overscan"
    bl_options = {"REGISTER"}

    # @classmethod
    # def poll(cls, context):
    #     return context.object

    init_rx : bpy.props.IntProperty(name='pre-overscan res x')
    init_ry : bpy.props.IntProperty(name='pre-overscan res y')

    def invoke(self, context, event):
        # if not context.object:
        #     self.report({'ERROR'}, 'No object selected')
        #     return {'CANCELLED'}

        self.use_selection = False
        if context.active_object and context.active_object.type == 'CAMERA' and context.active_object != context.scene.camera:
            self.use_selection = True
            self.cam_ob = context.active_object
        else:
            self.cam_ob = context.scene.camera

        self.init_rx = context.scene.render.resolution_x
        self.init_ry = context.scene.render.resolution_y

        return context.window_manager.invoke_props_dialog(self)

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

        if self.use_selection:
            col = layout.column()
            col.label(text=f'Camera "{self.cam_ob.name}" selected', icon='INFO')
            col.label(text='Change in shifts will apply on this one', icon='BLANK1')

        col = layout.column()
        col.label(text='Overscan res (current)')
        col.label(text=f'{context.scene.render.resolution_x} x {context.scene.render.resolution_y}')

        col = layout.column()
        col.label(text='Enter Initial (pre-overscan) resolution:')
        row = col.row(align=True)
        row.prop(self, 'init_rx', text='')
        row.prop(self, 'init_ry', text='')

    def execute(self, context):
        cam = self.cam_ob.data

        ratio_x = self.init_rx / context.scene.render.resolution_x
        ratio_y = self.init_ry / context.scene.render.resolution_y

        if ratio_x == 1 and ratio_y == 1:
            self.report({'ERROR'}, 'Same init and overscan resolution, nothing to change')
            return {'CANCELLED'}

        if ratio_x != 1:
            if fn.has_keyframe(cam, 'shift_x'):
                fcu = cam.animation_data.action.fcurves.find('shift_x')
                for k in fcu.keyframe_points:
                    k.co.y = k.co.y * ratio_x
            else:
                if cam.shift_x != 1:
                    cam.shift_x = cam.shift_x * ratio_x

        if ratio_y != 1:
            if fn.has_keyframe(cam, 'shift_y'):
                fcu = cam.animation_data.action.fcurves.find('shift_y')
                for k in fcu.keyframe_points:
                    k.co.y = k.co.y * ratio_y
            else:
                if cam.shift_y != 1:
                    cam.shift_y = cam.shift_y * ratio_y

        return {"FINISHED"}


# ui panel
class GPEXP_PT_extra_gprender_func(bpy.types.Panel):
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "output"
    bl_label = "GP Render Extras"
    bl_parent_id = "RENDER_PT_format" if (3, 0, 0) <= bpy.app.version else "RENDER_PT_dimensions"
    bl_options = {'DEFAULT_CLOSED'}
    # COMPAT_ENGINES = {'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}

    # def draw_header(self, context):
    #     overscan = context.scene.camera_overscan
    #     self.layout.prop(overscan, "RO_Activate", text="")

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.operator("gp.fix_overscan_shift")
        col.operator("gp.export_keys_to_ae")
        col.operator("gp.export_cam_keys_to_ae")

# def overcan_shift_fix_ui(self, context):
#     layout = self.layout
#     layout.operator("gp.fix_overscan_shift")

def export_ae_anim_menu(self, context):
    row = self.layout.row(align=False)
    row.operator('gp.export_anim_to_ae', text='After Effects Keyframe Data (.txt/.json)')
    # row.operator('gp.export_anim_to_ae', text='', icon='COPYDOWN')


classes=(
GPEXP_OT_export_keys_to_ae,
GPEXP_OT_export_cam_keys_to_ae,
GPEXP_OT_fix_overscan_shift,
GPEXP_PT_extra_gprender_func,
GPEXP_OT_export_anim_to_ae,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.TOPBAR_MT_file_export.append(export_ae_anim_menu)
    # if hasattr(bpy.types, 'RENDER_PT_overscan'):
    #     bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui)


def unregister():
    # if hasattr(bpy.types, 'RENDER_PT_overscan'):
    #     bpy.types.RENDER_PT_overscan.remove(overcan_shift_fix_ui)

    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    bpy.types.TOPBAR_MT_file_export.remove(export_ae_anim_menu)
