"""
    Flowblade Movie Editor is a nonlinear video editor.
    Copyright 2012 Janne Liljeblad.

    This file is part of Flowblade Movie Editor <https://github.com/jliljebl/flowblade/>.

    Flowblade Movie Editor is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Flowblade Movie Editor is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Flowblade Movie Editor. If not, see <http://www.gnu.org/licenses/>.
"""

"""
Handles syncing clips based on their audio data.
"""
import datetime
import hashlib
try:
    import mlt7 as mlt
except:
    import mlt
import os
import subprocess
import sys
import time
import threading

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import GLib
from gi.repository import Gtk, Gdk

import appconsts
import clapperless
import databridge
import dialogs
import dialogutils
import edit
import editorstate
from editorstate import PROJECT
from editorstate import current_sequence
import gui
import movemodes
import projectaction
import renderconsumer
import respaths
import updater
import userfolders
import utils

_tline_sync_data = None # Compound clip and tline clip sync functions can't pass the same data througn clapperless so 
                         # we use this global to save data as needed for tline sync function.
                         # The data flow is a bit haed to follow here, this needs tobe refactored.

_compare_dialog_thread = None

class ClapperlesLaunchThread(threading.Thread):
    def __init__(self, video_file, audio_file, completed_callback):
        threading.Thread.__init__(self)
        self.video_file = video_file
        self.audio_file = audio_file
        self.completed_callback = completed_callback
        
    def run(self):
        _write_offsets(self.video_file, self.audio_file, self.completed_callback)

# ------------------------------------------------------- module funcs
def _write_offsets(video_file_path, audio_file_path, completed_callback):
    print("Starting clapperless analysis...")
    fps = str(int(utils.fps() + 0.5))
    idstr = _get_offset_file_idstr(video_file_path, audio_file_path)

    FLOG = open(userfolders.get_cache_dir() + "log_clapperless", 'w')
    
    # clapperless.py computes offsets and writes them to file clapperless.OFFSETS_DATA_FILE
    p = subprocess.Popen([sys.executable, respaths.LAUNCH_DIR + "flowbladeclapperless", video_file_path, audio_file_path, "--rate", fps, "--idstr", idstr], stdin=FLOG, stdout=FLOG, stderr=FLOG)
    p.wait()
    
    # Offsets are now available
    GLib.idle_add(completed_callback, (video_file_path, audio_file_path, idstr))

def _get_offset_file_idstr(file_1, file_2):
    return hashlib.md5((file_1 + file_2).encode('utf-8')).hexdigest()
    
def _read_offsets(idstr):
    offsets_file = userfolders.get_cache_dir() + clapperless.OFFSETS_DATA_FILE + "_"+ idstr
    with open(offsets_file) as f:
        file_lines = f.readlines()
    file_lines = [x.rstrip("\n") for x in file_lines]
    
    _files_offsets = {}
    for line in file_lines:
        tokens = line.split(clapperless.MAGIC_SEPARATOR)
        _files_offsets[tokens[0]] = tokens[1]
    
    os.remove(offsets_file)

    return _files_offsets


# ------------------------------------------------------- tline audio sync
def init_select_tline_sync_clip(popup_data):

    clip, track, item_id, x = popup_data
    frame = databridge.tlinewidgets_get_frame(x)
    clip_index = current_sequence().get_clip_index(track, frame)

    if not (track.clips[clip_index] == clip):
        # This should never happen, give info.
        print("track.clips[clip_index] != clip) in init_select_tline_sync_clip, should not happen.") 
        return

    gdk_window = gui.tline_display.get_parent_window()
    gdk_window.set_cursor(Gdk.Cursor.new_for_display(Gdk.Display.get_default(), Gdk.CursorType.TCROSS))
    editorstate.edit_mode = editorstate.SELECT_TLINE_SYNC_CLIP

    global _tline_sync_data
    _tline_sync_data = TLineSyncData()
    _tline_sync_data.origin_clip = clip
    _tline_sync_data.origin_track = track
    _tline_sync_data.origin_clip_index = clip_index

def select_sync_clip_mouse_pressed(event, frame):
    sync_clip = _get_sync_tline_clip(event, frame)

    if sync_clip == None:
        return # selection wasn't good
    
    if utils.is_mlt_xml_file(sync_clip.path) == True:
        dialogutils.warning_message(_("Cannot Timeline Audio Sync with MLT XML Container Clips!"), 
                                    _("Audio syncing for MLT XML Container Clips is not supported."),
                                    gui.editor_window.window,
                                    True)
        return

    sync_track = databridge.tlinewidgets_get_track(event.y)
    sync_clip_index = sync_track.clips.index(sync_clip)

    _tline_sync_data.sync_clip = sync_clip
    _tline_sync_data.sync_track = sync_track
    _tline_sync_data.sync_clip_index = sync_clip_index

    # TImeline media offset for clips
    sync_clip_start_in_tline = sync_track.clip_start(sync_clip_index)
    _tline_sync_data.origin_clip_start_in_tline = _tline_sync_data.origin_track.clip_start(_tline_sync_data.origin_clip_index)
    
    _tline_sync_data.clip_tline_media_offset = (sync_clip_start_in_tline - sync_clip.clip_in) - (_tline_sync_data.origin_clip_start_in_tline - _tline_sync_data.origin_clip.clip_in)
    
    gdk_window = gui.tline_display.get_parent_window()
    gdk_window.set_cursor(Gdk.Cursor.new_for_display(Gdk.Display.get_default(), Gdk.CursorType.LEFT_PTR))
    
    global _compare_dialog_thread
    _compare_dialog_thread = AudioCompareActiveThread()
    
    # This or GUI freezes, we really can't do Popen.wait() in a Gtk thread
    clapperless_thread = ClapperlesLaunchThread(_tline_sync_data.origin_clip.path, sync_clip.path, _tline_sync_offsets_computed_callback)
    clapperless_thread.start()

    # Edit consumes selection
    movemodes.clear_selected_clips()

    updater.repaint_tline()
    
def _get_sync_tline_clip(event, frame):
    sync_track = databridge.tlinewidgets_get_track(event.y)

    if sync_track == None:
        return None
        
    if sync_track == _tline_sync_data.origin_track:
        dialogutils.warning_message(_("Audio Sync parent clips must be on different tracks "), 
                                _("Selected audio sync clip is on the sametrack as the sync action origin clip."),
                                gui.editor_window.window,
                                True)
        return None

    sync_clip_index = current_sequence().get_clip_index(sync_track, frame)
    if sync_clip_index == -1:
        return None

    return sync_track.clips[sync_clip_index]
    
def _tline_sync_offsets_computed_callback(clapperless_data):
    print("Clapperless done for tline sync")
    
    global _compare_dialog_thread
    _compare_dialog_thread.compare_done()
    
    file_path_1, file_path_2, idstr = clapperless_data
    files_offsets = _read_offsets(idstr)
    
    _tline_sync_data.media_offset_frames = int(float(files_offsets[file_path_2]) + 0.5)
    
    dialogs.tline_audio_sync_dialog(_tline_audio_sync_dialog_callback, _tline_sync_data)

def _tline_audio_sync_dialog_callback(dialog, response_id, data):
    if response_id != Gtk.ResponseType.ACCEPT:
        dialog.destroy()
        return
        
    dialog.destroy()

    sync_move_frames = _tline_sync_data.clip_tline_media_offset - _tline_sync_data.media_offset_frames
    over_in = _tline_sync_data.origin_clip_start_in_tline + sync_move_frames
    over_out = over_in + (_tline_sync_data.origin_clip.clip_out - _tline_sync_data.origin_clip.clip_in) + 1

    # We're not not supporting case where clip would start before timeline start.
    if over_in  < 0:
        primary_txt = _("Audio sync move not possible")
        secondary_txt = _("Clip starts ") + str(over_in) + _(" frames before timeline start if it is moved \nto be in audio sync with the specified clip.\n\n") + \
                        _("You need to move forward or shorten the clips in question to make the operation succeed.")
        dialogutils.info_message(primary_txt, secondary_txt, gui.editor_window.window)
        return

    data = {"track":_tline_sync_data.origin_track,
            "over_in":over_in,
            "over_out":over_out,
            "selected_range_in":_tline_sync_data.origin_clip_index,
            "selected_range_out":_tline_sync_data.origin_clip_index,
            "move_edit_done_func":None}

    action = edit.overwrite_move_action(data)
    action.do_edit()


class TLineSyncData:
    def __init__(self):
        # Origin clip
        self.origin_clip = None 
        self.origin_track = None
        self.origin_clip_index = None

        # Clip to sync origin clip
        self.sync_clip = None
        self.sync_track = None
        self.sync_clip_index = None

        # Timeline data
        self.origin_clip_start_in_tline = None
        self.clip_tline_media_offset = None
    
        # Media offset from clapperless
        self.media_offset_frames = None
        
        
# ------------------------------------------------------- compound clip audio sync
def create_audio_sync_compound_clip():
    selection = gui.media_list_view.get_selected_media_objects()
    if len(selection) != 2:
        return

    video_file = selection[0].media_file
    audio_file = selection[1].media_file
    
    # Can't sync compound clips
    if utils.is_mlt_xml_file(video_file.path) == True or utils.is_mlt_xml_file(audio_file.path) == True:

        dialogutils.warning_message(_("Cannot Create Audio Sync Clip from  MLT XML Container Clips!"), 
                                    _("Audio syncing MLT XML Container Clips is not supported."),
                                    gui.editor_window.window,
                                    True)
        return

    # Can't sync 2 audio clips
    if video_file.type == appconsts.AUDIO and audio_file.type == appconsts.AUDIO:

        dialogutils.warning_message(_("Cannot Create Audio Sync Container Clip from 2 Audio Clips!"), 
                                    _("One of the media items needs to be a video clip."),
                                    gui.editor_window.window,
                                    True)
        return
        
    if video_file.type == appconsts.VIDEO and audio_file.type == appconsts.AUDIO:
        pass
    elif video_file.type == appconsts.AUDIO and audio_file.type == appconsts.VIDEO:
        video_file, audio_file = audio_file, video_file
    else:
        print("2 video files, video audio assignments determined by selection order")

    global _compare_dialog_thread
    _compare_dialog_thread = AudioCompareActiveThread()
    
    # This or GUI freezes, we really can't do Popen.wait() in a Gtk thread
    clapperless_thread = ClapperlesLaunchThread(video_file.path, audio_file.path, _compound_offsets_complete)
    clapperless_thread.start()
    
def _compound_offsets_complete(data):
    print("Clapperless done for compound clip")

    global _compare_dialog_thread
    _compare_dialog_thread.compare_done()
    
    video_file_path, audio_file_path, idstr = data
    files_offsets = _read_offsets(idstr)
    sync_data = (files_offsets, data)

    # lets's just set default name to something unique-ish 
    default_name = _("SYNC_CLIP_") +  str(datetime.date.today()) + "_" + time.strftime("%H%M%S") + ".xml"
    dialogs.compound_clip_name_dialog(_do_create_sync_compound_clip, default_name, _("Save Sync Compound Clip XML"), sync_data)
    
def _do_create_sync_compound_clip(dialog, response_id, data):
    if response_id != Gtk.ResponseType.ACCEPT:
        dialog.destroy()
        return

    sync_data, name_entry = data
    files_offsets, clips = sync_data
    video_file, audio_file, idstr = clips
    media_name = name_entry.get_text()
    
    dialog.destroy()
    
    # Create unique file path in hidden render folder
    folder = userfolders.get_render_dir()
    uuid_str = hashlib.md5(str(os.urandom(32)).encode('utf-8')).hexdigest()
    write_file = folder + uuid_str + ".xml"
    
    # Create tractor
    tractor = mlt.Tractor()
    multitrack = tractor.multitrack()
    track_video = mlt.Playlist(PROJECT().profile)
    track_audio = mlt.Playlist(PROJECT().profile)
    track_audio.set("hide", 1) # video off, audio on as mlt "hide" value
    multitrack.connect(track_audio, 0)
    multitrack.connect(track_video, 0)

    # Create clips
    video_clip = mlt.Producer(PROJECT().profile, str(video_file)) 
    audio_clip = mlt.Producer(PROJECT().profile, str(audio_file))
    
    # Get offset
    offset = float(files_offsets[audio_file])
    
    # Add clips
    if offset > 0:
        offset_frames = int(float(offset) + 0.5)
        track_video.append(video_clip, 0, video_clip.get_length() - 1)
        track_audio.insert_blank(0, offset_frames)
        track_audio.append(audio_clip, 0, audio_clip.get_length() - 1)
    elif offset < 0:
        offset_frames = int(float(offset) - 0.5)
        track_video.insert_blank(0, offset_frames)
        track_video.append(video_clip, 0, video_clip.get_length() - 1)
        track_audio.append(audio_clip, 0, audio_clip.get_length() - 1)
    else:
        track_video.append(video_clip, 0, video_clip.get_length() - 1)
        track_audio.append(audio_clip, 0, audio_clip.get_length() - 1)

    # render MLT XML, callback in projectaction.py creates media object
    render_player = renderconsumer.XMLCompoundRenderPlayer(write_file, media_name, projectaction._xml_compound_render_done_callback, tractor, PROJECT())
    render_player.start()


# This is not a thread anymore but successive calls to Gdk.threads_add_timeout() 
# to achieve the same functionality.
class AudioCompareActiveThread:

    def __init__(self):
        self.running = True
        self.dialog = None
        
        Gdk.threads_add_timeout(GLib.PRIORITY_HIGH_IDLE, 10, self.start_dialog)
                
    def start_dialog(self):
        self.dialog = dialogs.audio_sync_active_dialog()
        self.dialog.progress_bar.set_pulse_step(0.2)
        time.sleep(0.1)

        Gdk.threads_add_timeout(GLib.PRIORITY_HIGH_IDLE, 200, self.run_dialog)
        
        return False
    
    def run_dialog(self):
        if self.running:
            self.dialog.progress_bar.pulse()
            return True
        else:
            PROJECT().update_media_lengths_on_load = False
            self.dialog.destroy()
            return False

    def compare_done(self):
        self.running = False

