import re
import cv2
import pytesseract
import pandas as pd
import requests
from io import BytesIO
from PIL import Image
import numpy as np
from concurrent.futures import ProcessPoolExecutor, as_completed
import multiprocessing
from functools import partial
import tqdm
import logging
import os

# ------------------------------
# Configuration and Mappings
# ------------------------------

# Configure logging
logging.basicConfig(
    filename='ocr_pipeline.log',
    filemode='a',
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=logging.INFO
)

# Configure Tesseract executable path
pytesseract.pytesseract.tesseract_cmd = '/opt/homebrew/bin/tesseract'

# Entity to unit mappings
entity_unit_map = {
    'width': {'centimetre', 'cm', 'foot', 'ft', 'inch', 'in', 'metre', 'm', 'millimetre', 'mm', 'yard', 'yd'},
    'depth': {'centimetre', 'cm', 'foot', 'ft', 'inch', 'in', 'metre', 'm', 'millimetre', 'mm', 'yard', 'yd'},
    'height': {'centimetre', 'cm', 'foot', 'ft', 'inch', 'in', 'metre', 'm', 'millimetre', 'mm', 'yard', 'yd'},
    'item_weight': {'gram', 'g', 'kilogram', 'kg', 'microgram', 'µg', 'milligram', 'mg', 'ounce', 'oz', 'pound', 'lb', 'ton', 't'},
    'maximum_weight_recommendation': {'gram', 'g', 'kilogram', 'kg', 'microgram', 'µg', 'milligram', 'mg', 'ounce', 'oz', 'pound', 'lb', 'ton', 't'},
    'voltage': {'kilovolt', 'kV', 'millivolt', 'mV', 'volt', 'V'},
    'wattage': {'kilowatt', 'kW', 'watt', 'W'},
    'item_volume': {
        'centilitre', 'cL', 'cubic foot', 'cubic inch', 'cup', 'decilitre', 'dL', 'fluid ounce', 'fl oz',
        'gallon', 'gal', 'imperial gallon', 'litre', 'L', 'microlitre', 'μL', 'millilitre', 'mL', 
        'pint', 'pt', 'quart', 'qt'
    }
}

# Abbreviation mappings
abbreviation_map = {
    'cm': 'centimetre', 'm': 'metre', 'mm': 'millimetre', 'ft': 'foot', 'in': 'inch', 'yd': 'yard',
    'kg': 'kilogram', 'g': 'gram', 'mg': 'milligram', 'µg': 'microgram', 'oz': 'ounce', 'lb': 'pound', 't': 'ton',
    'kV': 'kilovolt', 'mV': 'millivolt', 'V': 'volt',
    'kW': 'kilowatt', 'W': 'watt',
    'L': 'litre', 'mL': 'millilitre', 'cL': 'centilitre', 'dL': 'decilitre', 'μL': 'microlitre',
    'gal': 'gallon', 'fl oz': 'fluid ounce', 'pt': 'pint', 'qt': 'quart', 'cup': 'cup',
    'cubic foot': 'cubic foot', 'cubic inch': 'cubic inch', 'imperial gallon': 'imperial gallon'
}

# Entity keywords for identification
entity_keywords = {
    'width': ['width', 'breadth', 'wide'],
    'depth': ['depth'],
    'height': ['height', 'length', 'len'],
    'item_weight': ['weight', 'wt'],
    'maximum_weight_recommendation': ['maximum weight', 'recommended weight', 'max weight', 'max wt', 'maximum wt'],
    'voltage': ['voltage'],
    'wattage': ['wattage'],
    'item_volume': ['capacity', 'volume']
}

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

def preprocess_image(img, max_size=1024):
    """
    Resize image if the largest dimension exceeds max_size.
    """
    height, width = img.shape[:2]
    if max(height, width) > max_size:
        scaling_factor = max_size / float(max(height, width))
        img = cv2.resize(img, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
    return img

def extract_numerical_values(text, entity):
    """
    Extract numerical values and their units based on the entity.
    """
    units = entity_unit_map.get(entity, set())
    if not units:
        return 'void'
    
    # Escape units that have special regex characters
    units_escaped = [re.escape(unit) for unit in units]
    pattern = r'(\d+(.\d+)?)\s*(' + '|'.join(units_escaped) + r')\b'
    matches = re.findall(pattern, text, re.IGNORECASE)

    if matches:
        standardized_matches = [
            (
                float(match[0]),
                abbreviation_map.get(match[2].lower(), match[2].lower())
            ) for match in matches
        ]
        return standardized_matches
    return 'void'

def extract_dimensions_pattern(text):
    """
    Extract dimensions if present in the format 'LxWxH unit'.
    """
    pattern = r'(\d+(?:.\d+)?)x(\d+(?:.\d+)?)x(\d+(?:.\d+)?)\s*(mm|cm|in|ft|m)'
    match = re.search(pattern, text, re.IGNORECASE)

    if match:
        length_value = float(match.group(1))
        width_value = float(match.group(2))
        height_value = float(match.group(3))
        unit = abbreviation_map.get(match.group(4).lower(), match.group(4).lower())

        return {
            'length': (length_value, unit),
            'width': (width_value, unit),
            'height': (height_value, unit)
        }
    return {}

def extract_entity_values(text, query_entities):
    """
    Extract values for the specified entities from the given text.
    """
    entity_matches = {entity: 'void' for entity in query_entities}

    # Check for dimension patterns first
    dimension_matches = extract_dimensions_pattern(text)
    if dimension_matches:
        for entity in query_entities:
            if entity in dimension_matches:
                entity_matches[entity] = dimension_matches[entity]

    # Extract based on keywords if not found in dimensions
    for entity, keywords in entity_keywords.items():
        if entity in query_entities and entity_matches[entity] == 'void':
            for keyword in keywords:
                if re.search(r'\b' + re.escape(keyword) + r'\b', text, re.IGNORECASE):
                    numerical_values = extract_numerical_values(text, entity)
                    if numerical_values != 'void':
                        entity_matches[entity] = numerical_values
                        break

    # Fallback to any numerical values if still not found
    for entity in query_entities:
        if entity_matches[entity] == 'void':
            unit_matches = extract_numerical_values(text, entity)
            if unit_matches != 'void':
                entity_matches[entity] = unit_matches

    return entity_matches

def ocr_extract_entities(image_data, query_entities):
    """
    Perform OCR on the image data and extract specified entities.
    """
    try:
        img = Image.open(BytesIO(image_data))
        img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
        img = preprocess_image(img)

        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        # Apply thresholding to binarize the image
        _, thresh1 = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY_INV)

        # Define a rectangular kernel for dilation
        rect_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (18, 18))
        dilation = cv2.dilate(thresh1, rect_kernel, iterations=1)

        # Find contours
        contours, _ = cv2.findContours(dilation, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
        extracted_text = ""

        # Custom Tesseract configuration for speed
        custom_config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789.,cmftinmlyozkgsVW'

        for cnt in contours:
            x, y, w, h = cv2.boundingRect(cnt)
            cropped = img[y:y + h, x:x + w]
            text = pytesseract.image_to_string(cropped, config=custom_config)
            extracted_text += text + "\n"

        extracted_entities = extract_entity_values(extracted_text, query_entities)
        return extracted_entities
    except Exception as e:
        logging.error(f"OCR extraction failed: {e}")
        return {entity: 'void' for entity in query_entities}

def process_row(row):
    """
    Process a single row: download image, perform OCR, extract entity value.
    Returns a dictionary with index and prediction.
    """
    image_link = row['image_link']
    entity_name = row['entity_name']
    query_entities = [entity_name]

    try:
        response = requests.get(image_link, timeout=10)
        if response.status_code == 200:
            image_data = response.content
            extracted_entities = ocr_extract_entities(image_data, query_entities)
            entity_value = extracted_entities.get(entity_name, 'void')

            # Format the prediction based on extracted entity value
            if entity_value != 'void' and entity_value != []:
                if isinstance(entity_value, list):
                    numerical_value = f"{entity_value[0][0]:,.2f}"
                    unit = entity_value[0][1].lower()
                    prediction = f"{numerical_value} {unit}"
                elif isinstance(entity_value, tuple):
                    numerical_value = f"{entity_value[0]:,.2f}"
                    unit = entity_value[1].lower()
                    prediction = f"{numerical_value} {unit}"
                elif isinstance(entity_value, (float, int)):
                    prediction = f"{entity_value:,.2f}"
                else:
                    prediction = ''
            else:
                prediction = ''
        else:
            logging.warning(f"Failed to download image from {image_link}, Status Code: {response.status_code}")
            prediction = ''
    except Exception as e:
        logging.error(f"Error processing image {image_link}: {e}")
        prediction = ''

    return {'index': row.name, 'prediction': prediction}

# ------------------------------
# Main Processing Function
# ------------------------------

def main():
    input_file = '/Users/kartik/Downloads/student_resource 3/dataset/test.csv'
    output_file = 'output_final.csv'

    # Read input CSV without limiting rows
    try:
        df = pd.read_csv(input_file)
        logging.info(f"Successfully read {len(df)} entries from {input_file}")
    except Exception as e:
        logging.critical(f"Failed to read input file {input_file}: {e}")
        return

    # Prepare the output CSV
    # If the output file exists, remove it to avoid appending to old data
    if os.path.exists(output_file):
        os.remove(output_file)
        logging.info(f"Existing output file {output_file} found and removed.")

    # Define batch size for incremental saving
    batch_size = 10000  # Adjust based on available memory and performance
    total_entries = len(df)
    logging.info(f"Starting OCR processing of {total_entries} entries with batch size {batch_size}.")

    # Determine the number of workers based on CPU cores
    num_workers = multiprocessing.cpu_count()
    logging.info(f"Using {num_workers} worker processes for parallel processing.")

    # Open the output CSV and write headers
    with open(output_file, 'w') as f_out:
        f_out.write('index,prediction\n')

    # Create a generator to yield rows
    def row_generator(df):
        for _, row in df.iterrows():
            yield row

    # Process rows in batches
    with ProcessPoolExecutor(max_workers=num_workers) as executor:
        batch = []
        results = []
        futures = {}
        row_gen = row_generator(df)
        pbar = tqdm.tqdm(total=total_entries, desc="Processing Rows", unit="row")

        while True:
            try:
                # Fill the batch
                while len(batch) < batch_size:
                    row = next(row_gen)
                    batch.append(row)
            except StopIteration:
                # End of generator
                pass

            if not batch:
                break

            # Submit the batch to the executor
            for row in batch:
                future = executor.submit(process_row, row)
                futures[future] = row.name

            # Collect results as they complete
            for future in as_completed(futures):
                try:
                    result = future.result()
                    results.append(result)
                except Exception as e:
                    index = futures[future]
                    logging.error(f"Future at index {index} generated an exception: {e}")
                pbar.update(1)

            # Write the batch results to the CSV
            if results:
                df_results = pd.DataFrame(results)
                df_results.to_csv(output_file, mode='a', header=False, index=False)
                results = []

            # Clear the futures and batch for next iteration
            futures = {}
            batch = []

    pbar.close()

    logging.info(f"OCR processing completed. Results saved to {output_file}")

if __name__ == "__main__":
    main()

this is giving good results but i want to enhance it more how can i do it?