import os
from pathlib import Path
import shutil
import tempfile
from typing import List
import gradio as gr
import uuid
from typing_extensions import TypedDict, Tuple

from fastapi import FastAPI

app = FastAPI()

# create a static directory to store the static files
GS_DIR = Path(str(tempfile.gettempdir())) / "gaussian_splatting_gradio"
GS_DIR.mkdir(parents=True, exist_ok=True)

StateDict = TypedDict("StateDict", {
    "uuid": str,
})

# http://localhost:7860/file=/tmp/gradio/c2110a7de804b39754d229de426dc9307bc03aea/page.svelte

HOST = "localhost"
PORT = 7860

home_markdown = """
...
"""

step1_markdown = """
# Step 1 - Split Video into Frames

In the journey of transforming a video into a 3D Gaussian Splatting, the initial step is the conversion of the video into individual frames. You can here provide a **video file** and specify how much image you want to extract per second (*fps*). The application will then automatically extract the frames from the video and prepare them for the next step in the process.

However, you can also do this step manually and upload the frames directory by yourself in the next step. In this case, you can skip this step and go directly to the next step.

Please not that blurry frames will mostlikely result in a bad 3D model. So, make sure that the video is clear enough.
"""

step2_markdown = """
# Step 2 - SfM using Colmap

In this step we use Colmap (https://github.com/colmap/colmap). This process utilizes the frames extracted from the uploaded video to generate camera parameters and a point cloud, which are essential components for the 3D Gaussian Splatting process.

This step could take a while depending on the number of frames and the resolution. So, please be patient. 
You might want to do this step manually and upload the frames directory by yourself in the next step. In this case, you can skip this step and go directly to the next step.
"""

step3_markdown = """
# Step 3 - 3D Gaussian Splatting

In this final step we use the 3D Gaussian Splatting Cuda implementation by MrNeRF (https://twitter.com/janusch_patas): https://github.com/MrNeRF/gaussian-splatting-cuda.
As it's quite rapid to train, you can easily use a high number of iterations.
"""

def getPlyFile(session_state_value: StateDict) -> str:
    return f"/tmp/gaussian_splatting_gradio/{session_state_value['uuid']}/output/final_point_cloud.ply"

def getCamerasFile(session_state_value: StateDict) -> str:
    return f"/tmp/gaussian_splatting_gradio/{session_state_value['uuid']}/output/cameras.json"

def getZipFile(session_state_value: StateDict) -> str:
    return f"/tmp/gaussian_splatting_gradio/{session_state_value['uuid']}/result.zip"

def makeResult(session_state_value: StateDict) -> tuple[str, str, str]:
    ply_file = getPlyFile(session_state_value)
    cameras_file = getCamerasFile(session_state_value)
    zip_file = getZipFile(session_state_value)
    return [ply_file, cameras_file, zip_file]


# Utility functions
def createStateSession(previous_session: StateDict) -> StateDict:
    if previous_session["uuid"] is None:
        # Create new session
        session_uuid = str(uuid.uuid4())
        print("Creating new session: ", session_uuid)
        session_tmpdirname = GS_DIR / str(session_uuid)
        session_tmpdirname.mkdir(parents=True, exist_ok=True)
        print('Created temporary directory: ', session_tmpdirname)
        session = StateDict(
            uuid=session_uuid,
        )
    else:
        # Use previous session
        session = previous_session
    return session

def removeStateSession(session_state_value: StateDict):
    # Clean up previous session
    session_uuid = session_state_value["uuid"]
    session_tmpdirname = GS_DIR / str(session_uuid)
    print('Removing temporary directory: ', session_tmpdirname)
    shutil.rmtree(session_tmpdirname)
    return StateDict(
        uuid=None,
    )

def makeButtonVisible(btn_value: str) -> gr.Button:
    return gr.Button(btn_value, visible=True)


#  Process functions
def process_ffmpeg(
        session_state_value: StateDict,
        ffmpeg_input: str,
        ffmpeg_fps: int,
        ffmpeg_qscale: int,
    ) -> list[str]:
    # Ensure that a session is active
    if session_state_value["uuid"] is None:
        return

    # Set up session directory
    session_path = GS_DIR / str(session_state_value['uuid'])
    logfile_path = Path(session_path) / "ffmpeg_log.txt"
    logfile_path.touch()

    try:
        from services.ffmpeg import ffmpeg_run
        with logfile_path.open("w") as log_file:
            ffmpeg_run(
                video_path = Path(ffmpeg_input),
                output_path = session_path,
                fps = int(ffmpeg_fps),
                qscale = int(ffmpeg_qscale),
                stream_file=log_file
            )
        print("Done with ffmpeg")
    except Exception as e:
        print(f"Error - {e}")
        # print('Error - Removing temporary directory', session_path)
        # shutil.rmtree(session_path)
    # Get the list of all the file of (session_path / "input")
    list_of_jpgs = [str(f) for f in (session_path / "input").glob("*.jpg")]
    return list_of_jpgs

def processColmap(
        session_state_value: StateDict,
        colmap_inputs: List[tempfile.NamedTemporaryFile],
        colmap_camera: str,
        enable_rerun: bool
    ) -> Tuple[str, str]:
    # Ensure that a session is active
    if session_state_value["uuid"] is None:
        return "", ""
        
    # Set up session directory
    session_path = GS_DIR / str(session_state_value['uuid'])
    logfile_path = Path(session_path) / "colmap_log.txt"
    logfile_path.touch()

    rerunfile_path = Path(session_path) / "rerun_page.html"
    rerunfile_path.touch()

    (session_path / "input").mkdir(parents=True, exist_ok=True)
    for file in colmap_inputs:
        print("copying", file.name, "to", session_path / "input")
        shutil.copy(file.name, session_path / "input")

    try:
        from services.colmap import colmap
        with logfile_path.open("w") as log_file:
            colmap(
                source_path=session_path,
                camera=str(colmap_camera),
                stream_file=log_file
            )
        print("Done with colmap")
        
        if enable_rerun:
            from services.rerun import read_and_log_sparse_reconstruction
            html = read_and_log_sparse_reconstruction(
                exp_name = str(session_state_value['uuid']),
                dataset_path = session_path,
            )
            print("Done with rerun")
        else:
            html = "Rerun was disable !"
        with rerunfile_path.open("w") as rerunfile:
            rerunfile.write(html)
    except Exception as e:
        print(f"Error - {e}")
        # print('Error - Removing temporary directory', session_path)
        # shutil.rmtree(session_path)

    # zip the session_path folder
    archive = shutil.make_archive("result", 'zip', GS_DIR, session_path)
    print('Created zip file', archive)
    return archive, rerunfile_path

def processGaussianSplattingCuda(
        session_state_value: StateDict,
        gs_input: tempfile.NamedTemporaryFile,
        gs_iterations: int,
        gs_convergence_rate: float,
        gs_resolution: int,
    ) -> Tuple[str, str]:
    # Ensure that a session is active
    if session_state_value["uuid"] is None:
        return
    
    # Set up session directory
    session_path = GS_DIR / str(session_state_value['uuid'])
    logfile_path = Path(session_path) / "gaussian_splatting_cuda_log.txt"
    logfile_path.touch()

    # Unzip the gs_input file to the session_path
    shutil.unpack_archive(gs_input.name, session_path)

    # Copy the gs_input directory to the session_path
    # shutil.copytree(gs_input, session_path)

    try:
        from services.gaussian_splatting_cuda import gaussian_splatting_cuda
        with logfile_path.open("w") as log_file:
            gaussian_splatting_cuda(
                data_path = session_path,
                output_path = session_path / "output",
                gs_command = str(Path(__file__).parent.absolute() / "build" / 'gaussian_splatting_cuda'),
                iterations = int(gs_iterations),
                convergence_rate = float(gs_convergence_rate),
                resolution = int(gs_resolution),
                enable_cr_monitoring = False,
                force = False,
                empty_gpu_cache = False,
                stream_file = log_file
            )
        print("Done with gaussian_splatting_cuda")

        # Create a zip of the session_path folder
        archive = shutil.make_archive("result", 'zip', GS_DIR, session_path)
        print('Created zip file', archive)

        # Move the zip file to the session_path folder
        shutil.move(archive, session_path)
    except Exception as e:
        print(f"Error - {e}")
        # print('Error - Removing temporary directory', session_path)
        # shutil.rmtree(session_path)
    
    return (
        session_path / "output" / "final_point_cloud.ply",
        session_path / "output" / "cameras.json",
    )

def updateLog(logname:str, session_state_value: StateDict) -> str:
    if session_state_value["uuid"] is None:
        return ""

    log_file = GS_DIR / str(session_state_value['uuid']) / f"{logname}.txt"
    if not log_file.exists():
        return ""
    
    with log_file.open("r") as log_file:
        logs = log_file.read()

    return logs

def bindStep1Step2(step1_output: list[tempfile.NamedTemporaryFile]) -> list[str]:
    return [file.name for file in step1_output]

def bindStep2Step3(step2_output: tempfile.NamedTemporaryFile) -> str:
    return step2_output.name

def makeRerunIframe(rerun_html : tempfile.NamedTemporaryFile) -> str:
    # If rerun_html is bigger than 300MB, then we don't show it
    print(f"Rerun file size: {os.stat(rerun_html.name).st_size}")
    if os.stat(rerun_html.name).st_size > 100_000_000:
        print("Rerun file is too big, not showing it")
        return ""
    filepath = rerun_html.name
    print("filepath", filepath)
    return f"""<iframe src="/file={filepath}" width="100%"; height="1080px"></iframe>"""

with gr.Blocks() as demo:
    #############################
    ########## State ############
    #############################

    session_state = gr.State({
        "uuid": None,
    })

    #############################
    ###### UI Components ########
    #############################

    gr.Markdown("# Gaussian Splatting Kit")
    gr.Markdown("Click on the **Duplicate** button to create a new instance of this app.")
    duplicate_button = gr.DuplicateButton()
    gr.Markdown(value=home_markdown)

    with gr.Tab("Slit Video into Frames"):
        step1_description = gr.Markdown(step1_markdown)
        # Video Frames
        with gr.Row():   
            # Video Frames - Inputs
            with gr.Column():
                # Video Frames - Inputs - Video File
                step1_input = gr.PlayableVideo(
                    format="mp4",
                    source="upload",
                    label="Upload a video",
                    include_audio=False
                )
                # Video Frames - Inputs - Parameters
                with gr.Row(variant="panel"):
                    # Video Frames - Inputs - Parameters - FFMPEG FPS
                    step1_fps = gr.Number(
                        label="FFMPEG Fps",
                        value=1,
                        minimum=1,
                        maximum=5,
                        step=0.10,
                    )
                    # Video Frames - Inputs - Parameters - FFMPEG Qscale
                    step1_qscale = gr.Number(
                        label="FFMPEG Qscale",
                        value=1,
                        minimum=1,
                        maximum=5,
                        step=1,
                    )
            # Video Frames - Outputs
            with gr.Column():
                # Video Frames - Outputs - Video File
                step1_output = gr.File(
                    label="Frames",
                    file_count="directory",
                    type="file",
                    interactive=False,
                )
                # Video Frames - Outputs - Logs
                step1_logs = gr.Textbox(
                    label="Videos Logs",
                    interactive=False,
                    show_copy_button=True
                )
        # Video Frames - Process Button
        step1_processbtn = gr.Button("Process", visible=True)
        # Video Frames - Visualize
        # Video Frames - Visualize -
        # step1_visualize_gallery = gr.Gallery()
             
    with gr.Tab("Colmap"):
        step2_description = gr.Markdown(step2_markdown)
        # Colmap
        with gr.Row():
            # Colmap - Inputs
            with gr.Column():
                # Colmap - Inputs - Frames Directory
                step2_input = gr.File(
                    label="Upload a frames directory",
                    file_count="directory",
                    type="file",
                    interactive=True,
                )
                # Colmap - Inputs - Parameters
                with gr.Row(variant="panel"):
                    # Colmap - Inputs - Parameters - Colmap Camera
                    step2_camera = gr.Dropdown(
                        label="COLMAP Camera",
                        value="OPENCV",
                        choices=["OPENCV", "SIMPLE_PINHOLE", "PINHOLE", "SIMPLE_RADIAL", "RADIAL"],
                    )
                    # Colmap - Inputs - Parameters - Enable Rerun
                    step2_rerun = gr.Checkbox(
                        value=True,
                        label="Enable Rerun",
                    )
            # Colmap - Outputs
            with gr.Column():
                # Colmap - Outputs - Video File
                step2_output = gr.File(
                    label="Colmap",
                    file_count="single",
                    file_types=[".zip"],
                    type="file",
                    interactive=False,
                )
                # Colmap - Outputs - Logs
                step2_logs = gr.Textbox(
                    label="Colmap Logs",
                    interactive=False,
                    show_copy_button=True
                )
                
        # Colmap - Process Button
        step2_processbtn = gr.Button("Process", visible=True)

        # Colmap - Visualize
        # Colmap - Visualize - Rerun HTML File
        step_2_visualize_html = gr.File(
            label="Rerun HTML",
            file_count="single",
            file_types=[".html"],
            type="file",
            interactive=False,
            visible=False
        )
        # Colmap - Visualize - Rerun HTML
        step_2_visualize = gr.HTML("Rerun", visible=True)
    
    with gr.Tab("Gaussian Splatting"):
        step3_description = gr.Markdown(step3_markdown)
        # Gaussian Splatting
        with gr.Row():
            # Gaussian Splatting - Inputs
            with gr.Column():
                # Gaussian Splatting - Inputs - Colmap + Frames
                step3_input = gr.File(
                    label="Upload a colmap + frames directory",
                    file_count="single",
                    file_types=[".zip"],
                    type="file",
                    interactive=True,
                )
                # Gaussian Splatting - Inputs - Parameters
                with gr.Row(variant="panel"):
                    # Gaussian Splatting - Inputs - Parameters - GS Iterations
                    step3_iterations = gr.Number(
                        label="GS Iterations",
                        value=10_000,
                        minimum=1_000,
                        maximum=50_000,
                        step=1_000,
                    )
                    # Gaussian Splatting - Inputs - Parameters - GS Convergence Rate
                    step3_convergence_rate = gr.Number(
                        label="GS Convergence Rate",
                        value=0.01,
                        minimum=0.01,
                        maximum=1,
                        step=0.01,
                    )
                    # Gaussian Splatting - Inputs - Parameters - GS Resolution
                    step3_resolution = gr.Number(
                        label="GS Resolution",
                        value=512,
                        minimum=128,
                        maximum=1024,
                        step=128,
                    )
            # Gaussian Splatting - Outputs
            with gr.Column():
                with gr.Row():
                    # Gaussian Splatting - Outputs - PLY File
                    step3_output1 = gr.File(
                        label="PLY File",
                        file_count="single",
                        type="file",
                        interactive=False,
                    )
                
                    # Gaussian Splatting - Outputs - Cameras File
                    step3_output2 = gr.File(
                        label="Cameras File",
                        file_count="single",
                        type="file",
                        interactive=False,
                    )
                # Gaussian Splatting - Outputs - Logs
                step3_logs = gr.Textbox(
                    label="Gaussian Splatting Logs",
                    interactive=False,
                    show_copy_button=True
                )
        # Gaussian Splatting - Process Button
        step3_processbtn = gr.Button("Process", visible=True)
        # Gaussian Splatting - Visualize
        # Gaussian Splatting - Visualize - Antimatter15 HTML
        # step_3_visualize = gr.HTML(getAntimatter15HTML(), visible=True)
        step_3_visualize = gr.Button("Visualize", visible=True, link="https://antimatter15.com/splat/")

    #############################
    ########## Events ###########
    #############################
    ### Step 1
    # Make the process button visible when a video is uploaded
    step1_upload_event = step1_input.upload(
        fn=createStateSession,
        inputs=[session_state],
        outputs=[session_state]
    ).success(
        fn=makeButtonVisible,
        inputs=[step1_processbtn],
        outputs=[step1_processbtn],
    )
    # Do the processing when the process button is clicked
    step1_processevent = step1_processbtn.click(
        fn=process_ffmpeg,
        inputs=[session_state, step1_input, step1_fps, step1_qscale],
        outputs=[step1_output],
    ).success(
        fn=bindStep1Step2,
        inputs=[step1_output],
        outputs=[step2_input],
    ).success(
        fn=makeButtonVisible,
        inputs=[step2_processbtn],
        outputs=[step2_processbtn],
    )

    # Update the logs every 2 seconds
    step1_logsevent = step1_processbtn.click(
        fn=lambda session: updateLog("ffmpeg_log", session),
        inputs=[session_state],
        outputs=[step1_logs],
        every=2,
    )
    
    ## Step 2
    # Make the process button visible when a video is uploaded
    step2_upload_event = step2_input.upload(
        fn=createStateSession,
        inputs=[session_state],
        outputs=[session_state]
    ).success(
        fn=makeButtonVisible,
        inputs=[step2_processbtn],
        outputs=[step2_processbtn],
    )
    # Do the processing when the process button is clicked
    step2_processevent = step2_processbtn.click(
        fn=processColmap,
        inputs=[session_state, step2_input, step2_camera, step2_rerun],
        outputs=[step2_output, step_2_visualize_html]
    ).success(
        fn=bindStep2Step3,
        inputs=[step2_output],
        outputs=[step3_input],
    ).success(
        fn=makeButtonVisible,
        inputs=[step3_processbtn],
        outputs=[step3_processbtn],
    ).then(
        fn=makeRerunIframe,
        inputs=[step_2_visualize_html],
        outputs=[step_2_visualize],
    )

    # Update the logs every 2 seconds
    step2_logsevent = step2_processbtn.click(
        fn=lambda session: updateLog("colmap_log", session),
        inputs=[session_state],
        outputs=[step2_logs],
        every=2,
    )

    ## Step 3
    # Make the process button visible when a video is uploaded
    step3_upload_event = step3_input.upload(
        fn=createStateSession,
        inputs=[session_state],
        outputs=[session_state]
    ).success(
        fn=makeButtonVisible,
        inputs=[step3_processbtn],
        outputs=[step3_processbtn],
    )
    # Do the processing when the process button is clicked
    step3_processevent = step3_processbtn.click(
        fn=processGaussianSplattingCuda,
        inputs=[session_state, step3_input, step3_iterations, step3_convergence_rate, step3_resolution],
        outputs=[step3_output1, step3_output2]
    )
    # .success(
    #     fn=lambda x: x,
    #     inputs=[step3_output1, step3_output2],
    #     outputs=[],
    # )
    # Update the logs every 2 seconds
    step3_logsevent = step3_processbtn.click(
        fn=lambda session: updateLog("gaussian_splatting_cuda_log", session),
        inputs=[session_state],
        outputs=[step3_logs],
        every=2,
    )

    # reset_button = gr.ClearButton(
    #     components=[video_input, text_log, ffmpeg_fps, ffmpeg_qscale, colmap_camera],
    #     label="Reset",
    #     visible=False,
    # )
    # print(f"async (x) => {{ {getJS(url='http://0.0.0.0:7860/output/37c7ae54-7752-4e7b-8ba9-bab32c86b316/output/point_cloud/iteration_100/point_cloud.ply')} }}")
    
    # show_button.click(
    #     fn=None,
    #     inputs=[],
    #     outputs=[],
    #     _js=f"async (x) => {{ {getJS(url='http://0.0.0.0:7860/output/37c7ae54-7752-4e7b-8ba9-bab32c86b316/output/point_cloud/iteration_100/point_cloud.ply')} }}"
    # ).then(
    #     fn=None,
    #     inputs=[],
    #     outputs=[],
    #     _js=f"async (x) => {{ {getJS(url='http://0.0.0.0:7860/output/37c7ae54-7752-4e7b-8ba9-bab32c86b316/output/point_cloud/iteration_100/point_cloud.ply')} }}"
    # )

    # gr.LoginButton, gr.LogoutButton
    # gr.HuggingFaceDatasetSaver
    # gr.OAuthProfile

    # with gr.Tab("jsdn"):
    #     input_mic = gr.HTML(getRerunHTML())




demo.queue()
demo.launch()

# mount Gradio app to FastAPI app
# app = gr.mount_gradio_app(app, demo, path="/")


# if __name__ == "__main__":
#     uvicorn.run(app, host="0.0.0.0", port=7860, ws_max_size=16777216*1000)