from fnmatch import filter
from psp import MypSp
from argparse import Namespace
import torch
import utils, os, imageio, numpy as np, cv2, string, pickle as pkl
from torch import nn
from torch.profiler import profile, record_function, ProfilerActivity
import openvino as ov
import onnxruntime


class Tool:

    def __init__(self, opts=None, result_path='./data/results/', checkpoint_path='./best_model.pt'):
        torch.set_grad_enabled(False)
        
        if opts is None:
            self.opts = Namespace()
            self.opts.input_nc = 3
            self.opts.device = 'cpu'
            self.opts.test_batch_size = 2
            self.opts.checkpoint_path = checkpoint_path
            self.opts.result_path = result_path
        else:
            self.opts = opts
            self.opts.result_path = result_path
            os.makedirs(result_path, exist_ok=True)
        self.net = self.load_network()
        self.uploaded_images_path = './data/uploaded_images/'
        self.predefined_images_path = './data/'
        os.makedirs((self.uploaded_images_path), exist_ok=True)
        os.makedirs((result_path), exist_ok=True)
        self.chars = np.array((list(string.ascii_letters + string.digits + '_')), dtype=str)
        self.char_inds = np.arange(len(self.chars))
        self.alpha_blend_sigma = 5.0

    def get_predefined_images(self):
        image_files_ = os.listdir(self.predefined_images_path)
        image_files = []
        for suff in ('jpg', 'jpeg', 'png'):
            image_files += filter(image_files_, f"*.{suff}")

        return image_files

    def get_previously_uploaded_images(self):
        image_files_ = os.listdir(self.uploaded_images_path)
        image_files = []
        for suff in ('jpg', 'jpeg', 'png'):
            image_files += filter(image_files_, f"*.{suff}")

        return image_files

    def get_new_filename(self):
        fname_len = 20

        def get_rand_fname():
            ids = np.random.choice((self.char_inds), size=fname_len, replace=True)
            return ''.join(self.chars[ids])

        fname_ = get_rand_fname()
        print(f"generated name: {fname_}")
        while os.path.exists(os.path.join(self.uploaded_images_path, f"u{fname_}.png")):
            fname_ = get_rand_fname()

        return f"u{fname_}.png"

    def get_all_images(self):
        predefined_images = self.get_predefined_images()
        uploaded = self.get_previously_uploaded_images()
        return predefined_images + uploaded

    def load_torch_model(self):
        net = MypSp(self.opts)
        net.encoder = torch.quantization.quantize_dynamic(
            net.encoder, {nn.Linear}, dtype=torch.qint8
        )
        net.load_state_dict(torch.load('quant_model.pt'))
        net.eval()
        # net = ipex.optimize(
        #     net,
        #     dtype=torch.qint8,
        #     weights_prepack=True
        # )
        return net

    def load_ov_model(self):
        ov_model_path = 'openvino_model/compressed_model.xml'
        core = ov.Core()
        model = core.read_model(model=ov_model_path)
        compiled_model = core.compile_model(model=model, device_name="CPU")
        return compiled_model

    def load_onnx_model(self):
        session_options = onnxruntime.SessionOptions()
        session_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL

        onnx_session = onnxruntime.InferenceSession(
            'styledreamer_quantized_model.onnx',
            sess_options=session_options,
            providers=['CPUExecutionProvider']
        )
        return onnx_session

    def get_onnx_output(self, face_image, hair_image):
        # Prepare inputs for the ONNX model
        onnx_input = {
            self.net.get_inputs()[0].name: face_image.cpu().numpy(),
            self.net.get_inputs()[1].name: hair_image.cpu().numpy(),
        }
        # Run ONNX model
        onnx_output = self.net.run(None, onnx_input)
        return torch.tensor(onnx_output[0])
        
    def get_absolute_path(self, fname):
        fname = fname.split('/')[(-1)]
        if fname.startswith('u'):
            return os.path.join(self.uploaded_images_path, fname)
        else:
            return os.path.join(self.predefined_images_path, fname)

    def __prepare_filename(self, nm1, nm2, blend, kwargs={'phase': 'ht'}):
        fname1_ws = nm1.split('/')[(-1)]
        fname1_wos = fname1_ws.split('.')[0]
        if nm2 is not None:
            fname2_ws = nm2.split('/')[(-1)]
            fname2_wos = fname2_ws.split('.')[0]
        if 'ht' in kwargs['phase']:
            new_filename = os.path.join(self.opts.result_path, f"{fname1_wos}_{fname2_wos}_blend_{blend}.jpg")
        else:
            if 'interp' in kwargs['phase']:
                os.makedirs((os.path.join(self.opts.result_path, 'interp')), exist_ok=True)
                new_filename = os.path.join(self.opts.result_path, 'interp', f"{fname1_wos}_{fname2_wos}_{kwargs['curr_step']}_{kwargs['max_step']}_blend_{blend}_hair_{kwargs['hair']}.jpg")
            else:
                if 'manip' in kwargs['phase']:
                    os.makedirs((os.path.join(self.opts.result_path, 'manip')), exist_ok=True)
                    new_filename = os.path.join(self.opts.result_path, 'manip', f"{fname1_wos}_{kwargs['dir']}_{kwargs['strength']}_blend_{blend}.jpg")
        return new_filename

    def hairstyle_transfer(self, im1_path, im2_path, alpha_blend=True):
        nonblended_name = self.__prepare_filename(im1_path, im2_path, False)
        blended_name = self.__prepare_filename(im1_path, im2_path, True)
        thefilename = blended_name if alpha_blend else nonblended_name
        with profile(activities=[ProfilerActivity.CPU], profile_memory=True, record_shapes=True) as prof:
            if os.path.exists(thefilename):
                return thefilename
            else:
                if self.net is None:
                    print('Model was not loaded. Loading default model')
                    self.load_network()
                else:
                    # self.net.to(self.opts.device)
                    if alpha_blend:
                        failed_part = []
                        
                        try:
                            inner_face2, hair2, mask2 = utils.load_and_split_image(im2_path, return_mask=True)
                        except ValueError as e:
                            failed_part.append('face')
                        try:
                            inner_face1, hair1, mask1 = utils.load_and_split_image(im1_path, return_mask=True)
                        except ValueError as e:
                            failed_part.append('hair')

                        if len(failed_part) > 0:
                            return None, failed_part
                    else:
                        inner_face1, hair1 = utils.load_and_split_image(im1_path)
                    inner_face2, hair2 = utils.load_and_split_image(im2_path)
                # print(f"Range of inner face: [{inner_face2.min()}, {inner_face2.max()}]")
                
                # Wrap the inference part with torch.no_grad() for efficiency
                with torch.no_grad():
                    input_face = utils.numpy_uint8_to_torch(inner_face2.astype(np.uint8))
                    input_hair = utils.numpy_uint8_to_torch(hair1.astype(np.uint8))
                    if alpha_blend:
                        masks = [mask2]
                        im_orig = [inner_face2 + hair2]
                    # output = self.net.forward(input_face, input_hair, resize=False)
                    with record_function('model_inference'):
                        output = torch.from_numpy(self.net([input_face, input_hair])[0])
                output_images = output.cpu()
                    
                for ii in range(output_images.shape[0]):
                    img_generated = utils.torch_to_numpy_uint8(output_images[ii:ii + 1])
                    if alpha_blend:
                        img_original = im_orig[ii]
                        im_mask = utils.gaussian_filter((masks[ii]), sigma=(self.alpha_blend_sigma))
                        # print(f"Range of image mask: [{im_mask.min()}, {im_mask.max()}]")
                        if img_original.shape[0] < 1024:
                            img_original = cv2.resize(img_original, (1024, 1024))
                        if im_mask.shape[0] < 1024:
                            im_mask = cv2.resize(im_mask, (1024, 1024))
                        masked_output = utils.alpha_blend_images(img_generated, img_original, im_mask).astype(np.uint8)
                        # print(f"Range of masked output: [{masked_output.min()}, {masked_output.max()}]")
                        imageio.imwrite(blended_name, masked_output)
                    imageio.imwrite(nonblended_name, img_generated)
        print(prof.key_averages().table(sort_by='cpu_time_total', row_limit=10))
        return thefilename, []
        
te,profiler,return,total,time,is,8,s,while,from fastapi import FastAPI, File, UploadFile, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse, RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.templating import Jinja2Templates
from PIL import Image  # For resizing images
from hairstyle_transfer_tool import Tool
from datetime import datetime
from pathlib import Path
import logging
import time
import os
import io  # For working with file bytes

with open('/proc/cpuinfo', 'r') as f:
    for line in f:
        if "model name" in line:
            print(line.strip().split(': ')[1])

app = FastAPI()
logger = logging.getLogger("uvicorn.error")

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Allows all origins; you can specify a list of allowed origins instead
    allow_credentials=True,
    allow_methods=["*"],  # Allows all HTTP methods (GET, POST, etc.)
    allow_headers=["*"],  # Allows all headers
)

# Initialize your tool once, at the module level
tool = Tool(opts=None, result_path='./data/results/', checkpoint_path='./best_model.pt')

UPLOADED_IMAGES_DIR = './data/uploaded_images'
os.makedirs(UPLOADED_IMAGES_DIR, exist_ok=True)

@app.get("/")
async def hello():
    return "Hello, have your virtual haircut with StyleDreamer!"

def resize_image(image_file, size=(256, 256)):
    """
    Resizes the uploaded image to the specified size.
    """
    image = Image.open(io.BytesIO(image_file))
    image = image.resize(size)
    img_byte_arr = io.BytesIO()
    image.save(img_byte_arr, format='PNG')
    return img_byte_arr.getvalue()

@app.post("/run-inference")
async def get_haircut(source: UploadFile = File(...), target: UploadFile = File(...)):
    current_time = time.time()
    time_in_seconds = int(current_time)
    decimal_part = current_time - time_in_seconds
    decimal_part = f'{decimal_part:.10f}'[1:]
    id = f"{datetime.now().strftime('%Y%m%d')}_{str(time_in_seconds)[-3:]}{decimal_part}"

    try:
        # # Resize and save the source and target images
        # source_resized = resize_image()  # Resize to 256x256
        # target_resized = resize_image()  # Resize to 256x256

        source_filepath = os.path.join(UPLOADED_IMAGES_DIR, f'hair_{id}_{source.filename}')
        target_filepath = os.path.join(UPLOADED_IMAGES_DIR, f'face_{id}_{target.filename}')

        with open(source_filepath, 'wb') as f:
            f.write(source.file.read())
        with open(target_filepath, 'wb') as f:
            f.write(target.file.read())

        # Measure the time taken for hairstyle transfer
        start_time = time.time()  # Start time

        # Perform the hairstyle transfer
        result_file_path, failed_part = tool.hairstyle_transfer(
            source_filepath,
            target_filepath,
            # alpha_blend=True
            alpha_blend=False
        )

        # Calculate the duration
        end_time = time.time()  # End time
        duration = end_time - start_time
        print(f"Hairstyle transfer took {duration:.2f} seconds.")

        # Check if there are any failed parts
        if len(failed_part) > 0:
            message = f"Can't see your {' and '.join(failed_part)} image clearly. Ensure the image uploaded is not obstructed, well-lit, and facing the camera."
            return JSONResponse(
                content={
                    "status": {
                        "code": 400,
                        "message": message,
                    },
                    "data": None
                },
                status_code=200
            )

        # Verify that the result file exists
        if not os.path.exists(result_file_path):
            return JSONResponse(
                content={
                    "status": {
                        "code": 404,
                        "message": "Result image not found, please try again",
                    },
                    "data": None
                },
                status_code=200
            )

        # Respond with the result image file
        return FileResponse(result_file_path)

    except Exception as e:
        # General exception handling, return error response
        logger.error(f"Error during inference: {e}", exc_info=True)
        return JSONResponse(
            content={
                "status": {
                    "code": 500,
                    "message": f"An error occurred: {str(e)}",
                },
                "data": None
            },
            status_code=500
        )


RESULT_IMAGE_DIR = Path('./data/results')
UPLOADED_IMAGE_DIR = Path('./data/uploaded_images')
templates = Jinja2Templates(directory="app/templates")

@app.get("/dashboard", response_class=HTMLResponse)
async def read_root(request: Request):
    hair_images = list(UPLOADED_IMAGE_DIR.glob("hair_*"))
    face_images = list(UPLOADED_IMAGE_DIR.glob("face_*"))
    result_images = list(RESULT_IMAGE_DIR.glob("*"))
    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "hair_images": hair_images,
        "face_images": face_images,
        "result_images": result_images,
    })

@app.post("/dashboard/uploaded/delete/{filename}")
async def delete_uploaded_image(filename: str):
    file_path = UPLOADED_IMAGE_DIR / filename
    if not file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    file_path.unlink()
    return RedirectResponse("/dashboard", status_code=303)

@app.get("/dashboard/uploaded/download/{filename}")
async def download_uploaded_image(filename: str):
    file_path = UPLOADED_IMAGE_DIR / filename
    if not file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(file_path, media_type='application/octet-stream', filename=filename)
    
@app.post("/dashboard/result/delete/{filename}")
async def delete_result_image(filename: str):
    file_path = RESULT_IMAGE_DIR / filename
    if not file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    file_path.unlink()
    return RedirectResponse("/dashboard", status_code=303)

@app.get("/dashboard/result/download/{filename}")
async def download_result_image(filename: str):
    file_path = RESULT_IMAGE_DIR / filename
    if not file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(file_path, media_type='application/octet-stream', filename=filename),from,main,the,time,module,say,13,s,why,there,is,gap,between