import re
import mss
from PIL import Image, ImageEnhance, ImageFilter
import pytesseract
import speech_recognition as sr
import pyttsx3
import keyboard
import threading
import os
import logging
import tkinter as tk
import ctypes
import sys
import ollama
import asyncio
from datetime import datetime
from word2number import w2n  # Optional for number word conversion
import re

# -------------------- Configuration --------------------

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.StreamHandler(),
    ]
)

tts_engine = pyttsx3.init()
is_listening = False
ocr_text = ""

lock = threading.Lock()
tts_lock = threading.Lock()

OUTPUT_DIR = (r"D:\\AI\\EXP2\\TempS")

def set_dpi_awareness():
    try:
        if sys.platform.startswith('win'):
            awareness = ctypes.c_int()
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
            logging.info("Process DPI awareness set to SYSTEM_AWARE.")
    except Exception as e:
        logging.warning(f"Could not set DPI awareness: {e}. Continuing without setting DPI awareness.")

def get_scaling_factor():
    try:
        if sys.platform.startswith('win'):
            user32 = ctypes.windll.user32
            try:
                user32.SetProcessDPIAware()
                dpi = user32.GetDpiForWindow(user32.GetForegroundWindow())
                scaling_factor = dpi / 96
                logging.info(f"DPI Scaling Factor detected: {scaling_factor}")
                return scaling_factor
            except AttributeError:
                user32.SetProcessDPIAware()
                hdc = user32.GetDC(0)
                LOGPIXELSX = 88
                dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, LOGPIXELSX)
                scaling_factor = dpi / 96
                ctypes.windll.user32.ReleaseDC(0, hdc)
                logging.info(f"DPI Scaling Factor detected via fallback: {scaling_factor}")
                return scaling_factor
        else:
            logging.info("Non-Windows OS detected. Defaulting DPI scaling factor to 1.0.")
            return 1.0
    except Exception as e:
        logging.warning(f"Could not retrieve DPI scaling factor: {e}. Defaulting to 1.0.")
        return 1.0
    
def convert_number_words(text):
    """
    Converts number words in the speech text to digits.
    Example: "five hundred ninety-seven" -> "597"
    """
    try:
        # Attempt to convert the entire text
        number = w2n.word_to_num(text)
        return str(number)
    except:
        # Attempt to extract digits directly if conversion fails
        digits = re.findall(r'\d+', text)
        return ''.join(digits) if digits else text  # Return the number if found, else original text



def speak_text(text):
    global tts_engine
    logging.info(f"Speaking out the response: {text}")
    with tts_lock:
        try:
            tts_engine.say(text)

            # Add hotkeys for pausing (F4) and stopping (F5)
            keyboard.add_hotkey('f4', pause_speaking)
            keyboard.add_hotkey('f5', stop_speaking)

            tts_engine.runAndWait()
            logging.info("Text-to-Speech completed.")
        except RuntimeError as e:
            logging.error(f"TTS Error: {e}")
        finally:
            keyboard.remove_hotkey('f4')
            keyboard.remove_hotkey('f5')

def pause_speaking():
    global tts_engine
    logging.info("Paused speaking...")
    tts_engine.pause()

def stop_speaking():
    global tts_engine
    logging.info("Stopped speaking.")
    tts_engine.stop()

def save_captured_image(image, output_dir, prefix="captured_region"):
    try:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(output_dir, f"{prefix}_{timestamp}.png")
        image.save(filename)
        logging.info(f"Captured image saved as {filename}")
    except Exception as e:
        logging.error(f"Failed to save captured image: {e}")

def save_preprocessed_image(image, output_dir, prefix="preprocessed_image"):
    try:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(output_dir, f"{prefix}_{timestamp}.png")
        image.save(filename)
        logging.info(f"Preprocessed image saved as {filename}")
    except Exception as e:
        logging.error(f"Failed to save preprocessed image: {e}")

def perform_ocr(image):
    global ocr_text
    try:
        logging.info("Starting OCR process with image preprocessing...")

        save_captured_image(image, OUTPUT_DIR)

        gray = image.convert('L')
        logging.debug("Converted image to grayscale.")

        enhancer = ImageEnhance.Contrast(gray)
        gray_enhanced = enhancer.enhance(1.0)
        logging.debug("Enhanced image contrast.")

        threshold = 190
        gray_threshold = gray_enhanced.point(lambda x: 0 if x < threshold else 255, '1')
        logging.debug("Applied thresholding to binarize the image.")

        final_image = gray_threshold

        save_preprocessed_image(final_image, OUTPUT_DIR)

        logging.info("Image preprocessing complete. Performing OCR...")

        custom_config = r'--oem 3 --psm 6'
        raw_text = pytesseract.image_to_string(final_image, lang='eng', config=custom_config)

        if not raw_text.strip():
            logging.warning("OCR returned empty text.")
            speak_text("I couldn't detect any text in the selected area. Please try again with a different region.")
        else:
            ocr_text = raw_text
            logging.debug("Raw OCR Text:")
            logging.debug(ocr_text)
            logging.info("OCR process completed successfully.")
    except pytesseract.TesseractNotFoundError:
        logging.error("Tesseract executable not found. Please ensure Tesseract is installed and PATH is set correctly.")
        speak_text("Tesseract is not installed or not found. Please install Tesseract and try again.")
        ocr_text = ""
    except Exception as e:
        logging.error(f"OCR Error: {e}")
        speak_text("An error occurred during OCR processing. Please try again.")
        ocr_text = ""

def get_selected_region(scaling_factor):
    root = tk.Tk()
    root.title("Select Region (Press Esc to Cancel)")
    root.attributes('-alpha', 0.3)
    root.attributes("-topmost", True)
    root.overrideredirect(True)

    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    root.geometry(f"{screen_width}x{screen_height}")

    canvas = tk.Canvas(root, cursor="cross", bg="grey")
    canvas.pack(fill=tk.BOTH, expand=True)

    rect = None
    start_x = start_y = end_x = end_y = 0

    def on_button_press(event):
        nonlocal start_x, start_y, rect
        start_x = int(event.x * scaling_factor)
        start_y = int(event.y * scaling_factor)
        rect = canvas.create_rectangle(event.x, event.y, event.x, event.y, outline='red', width=2)
        logging.debug(f"Button pressed at ({start_x}, {start_y})")

    def on_move_press(event):
        nonlocal end_x, end_y
        end_x = int(event.x * scaling_factor)
        end_y = int(event.y * scaling_factor)
        canvas.coords(rect, start_x / scaling_factor, start_y / scaling_factor, event.x, event.y)
        logging.debug(f"Mouse moved to ({end_x}, {end_y})")

    def on_button_release(event):
        root.quit()
        root.destroy()

    def on_escape(event):
        logging.info("Capture cancelled by user.")
        root.quit()
        root.destroy()
        sys.exit()

    canvas.bind("<ButtonPress-1>", on_button_press)
    canvas.bind("<B1-Motion>", on_move_press)
    canvas.bind("<ButtonRelease-1>", on_button_release)
    root.bind("<Escape>", on_escape)

    logging.info("Please select the region by clicking and dragging the mouse.")
    root.mainloop()

    left = min(start_x, end_x)
    top = min(start_y, end_y)
    right = max(start_x, end_x)
    bottom = max(start_y, end_y)

    logging.debug(f"Selected region coordinates (scaled): Left={left}, Top={top}, Right={right}, Bottom={bottom}")
    return (left, top, right, bottom)

def generate_final_response(ocr_text):
    logging.info("Generating final response...")
    
    context = f"""Assume the role of a strategist who excels at logical problem-solving and planning based on OCR Text. Your responses should prioritize efficiency, clarity, and foresight in addressing challenges.

Behavior: Break down complex problems into manageable parts and outline step-by-step strategies. Approach each situation with a calculated, analytical mindset, ensuring thorough analysis before offering solutions.

Mannerisms: Use structured, organized language. Ask clarifying questions to gather essential information and present your thoughts in a clear, methodical manner..
OCR Text:
{ocr_text}

Instructions:
1. Understand the OCR text and treat it as a comprehensive multiple-choice question.
2. Identify the correct answer from options (if provided within text).
3. Respond in the following format: "The answer is ((X)Answer) because (reason)".
4. Elaborate on the reason briefly, explaining why this is the correct answer.
5.Ignore these words if you come across them in OCR text like they do not exist : GPSC, GWSSB, GSSSB, GPSSB, GMB, PGVCL, MGVCL,
GSECL, GETCO, GMC, GIDC, GPHC, GMDC,NWRSW&K
6.Keep your answer strictly to 100 words
7.If there is calculation involved then follow this : We have been given (calculation_data), The formula to be used is (formula), we need to find (answer_variable)
8. Do calculation attentively
Provide the answer below:
"""
    try:
        # Assume the response is structured correctly by the LLM
        response = ollama.generate(model='mistral-nemo', prompt=context)
        raw_response = response.get('response', '').strip()

        # Perform basic checks to ensure a valid response format
        if "because" in raw_response:
            formatted_answer = raw_response
            logging.info(f"Generated Formatted Response: {formatted_answer}")
        else:
            formatted_answer = "The response could not be structured correctly."

        # Provide the response
        if formatted_answer:
            speak_text(formatted_answer)
        else:
            logging.warning("No valid response could be generated.")
        
        return formatted_answer
    except Exception as e:
        logging.error(f"LLM Error during final response generation: {e}")
        return ""

def reset_state():
    global ocr_text
    ocr_text = ""

# -------------------- Helper Functions --------------------


def transcribe_speech(short_timeout=5, phrase_time_limit=5):
    """
    Transcribes speech from the microphone with a short timeout.
    If no speech is detected within the timeout, proceeds without user input.

    Args:
        short_timeout (int): Maximum number of seconds to wait for a phrase to start.
        phrase_time_limit (int): Maximum number of seconds a phrase can last.
    """
    global is_listening, speech_text
    recognizer = sr.Recognizer()
    with sr.Microphone() as source:
        logging.info("Listening for voice input... Please say your query or question.")
        recognizer.adjust_for_ambient_noise(source, duration=1)  # Adjust for noise for 1 second
        try:
            # Listen for speech with shorter timeout and phrase time limit
            audio = recognizer.listen(source, timeout=short_timeout, phrase_time_limit=phrase_time_limit)
            logging.info("Processing the audio with Google Speech Recognition...")
            text = recognizer.recognize_google(audio)
            # Convert number words to digits if necessary
            speech_text = convert_number_words(text)
            logging.info(f"Speech Transcribed: {speech_text}")

            # Provide confirmation
            speak_text(f"I heard: {speech_text}.")
        except sr.WaitTimeoutError:
            logging.warning("Listening timed out, no speech detected. Proceeding without voice input.")
            speech_text = ""  # Ensure speech_text is empty
        except sr.UnknownValueError:
            logging.warning("Speech recognition could not understand audio. Proceeding without voice input.")
            speech_text = ""  # Ensure speech_text is empty
        except sr.RequestError as e:
            logging.error(f"Could not request results from speech recognition service; {e}")
            speech_text = ""  # Ensure speech_text is empty
        finally:
            with lock:
                is_listening = False  # Ensure listening flag is reset


def configure_tts_voice():
    """
    Configures the TTS engine's voice, rate, and volume.
    """
    global tts_engine
    voices = tts_engine.getProperty('voices')

    # List available voices (optional)
    for index, voice in enumerate(voices):
        logging.debug(f"Voice {index}: {voice.name}, {voice.gender}, {voice.languages}")

    # Change voice (select male/female, adjust the index as per your preference)
    if len(voices) > 1:
        tts_engine.setProperty('voice', voices[1].id)  # Change index to select a different voice
        logging.info(f"TTS voice set to: {voices[1].name}")
    else:
        logging.info("Only one voice available. Using default voice.")

    # Adjust speaking rate (lower is slower and more human-like)
    tts_engine.setProperty('rate', 150)  # Default is 200, lowering makes it more human-like

    # Adjust volume (0.0 to 1.0)
    tts_engine.setProperty('volume', 1.0)

def capture_and_save_image():
    global ocr_text

    region = get_selected_region(scaling_factor)
    left, top, right, bottom = region
    width = right - left
    height = bottom - top

    logging.debug(f"Width: {width}, Height: {height}, Scaling Factor: {scaling_factor}")

    if width <= 0 or height <= 0:
        logging.error("Invalid region selected. Please try again.")
        speak_text("Invalid region selected. Please try again by pressing F2.")
        reset_state()
        return

    with mss.mss() as sct:
        monitor = {
            "top": int(top / scaling_factor),
            "left": int(left / scaling_factor),
            "width": int(width / scaling_factor),
            "height": int(height / scaling_factor)
        }

        logging.debug(f"Monitor for capture: {monitor}")

        try:
            screenshot = sct.grab(monitor)
            img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
        except Exception as e:
            logging.error(f"Screen capture failed: {e}")
            speak_text("Screen capture failed. Please ensure the selected area is correct and try again.")
            reset_state()
            return

    perform_ocr(img)
    if not ocr_text.strip():
        logging.error("OCR returned empty text. Please ensure the selected region contains readable text.")
        speak_text("OCR did not detect any text. Please ensure the selected area contains readable text and try again.")
        reset_state()
        return

    generate_final_response(ocr_text)
    reset_state()

def start_listening(scaling):
    global is_listening
    with lock:
        if not is_listening:
            is_listening = True
            logging.info("Started listening...")
            threading.Thread(target=capture_and_save_image, args=(), daemon=True).start()

def stop_listening():
    global is_listening
    with lock:
        if is_listening:
            is_listening = False
            logging.info("Stopped listening.")
            speak_text("Stopped listening.")

async def main():
    global scaling_factor

    configure_tts_voice()

    set_dpi_awareness()
    scaling_factor = get_scaling_factor()

    keyboard.add_hotkey('f2', start_listening, args=(scaling_factor,))
    keyboard.add_hotkey('f3', stop_listening)

    logging.info("QA Assistant is running.")
    logging.info("Press 'F2' to select a region and answer a question, 'F3' to stop listening, 'Esc' to exit.")

    while True:
        await asyncio.sleep(0.1)
        if keyboard.is_pressed('esc'):
            logging.info("Exiting...")
            speak_text("Exiting the QA Assistant. Goodbye!")
            break

    keyboard.unhook_all()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logging.info("Program terminated by user.")
    except Exception as e:
        logging.error(f"An unexpected error occurred: {e}")
    finally:
        keyboard.unhook_all():::::::::::::::Remove the [DEGUB] output from terminal as they are cluttering the output and make it open to input via f2 after the LLM responce has been dictated, so that I don't have to run the script everytime