import { createWorker } from 'tesseract.js';

export interface OpusMagnumData {
  puzzle: string;
  cost: string;
  cycles: string;
  area: string;
}

export interface OCRRegion {
  x: number;
  y: number;
  width: number;
  height: number;
}

export class OpusMagnumOCRService {
  private worker: Tesseract.Worker | null = null;
  private availablePuzzleNames: string[] = [];

  // Regions based on main.py coordinates (adjusted for web usage)
  private readonly regions: Record<string, OCRRegion> = {
    puzzle: { x: 15, y: 600, width: 330, height: 28 },
    cost: { x: 412, y: 603, width: 65, height: 22 },
    cycles: { x: 577, y: 603, width: 65, height: 22 },
    area: { x: 739, y: 603, width: 65, height: 22 }
  };

  async initialize(): Promise<void> {
    if (this.worker) return;
    
    this.worker = await createWorker('eng');
    await this.worker.setParameters({
      tessedit_ocr_engine_mode: '3',
      tessedit_pageseg_mode: 7 as any
    });
  }

  /**
   * Set the list of available puzzle names for better OCR matching
   */
  setAvailablePuzzleNames(puzzleNames: string[]): void {
    this.availablePuzzleNames = puzzleNames;
  }

  async extractOpusMagnumData(imageFile: File): Promise<OpusMagnumData> {
    if (!this.worker) {
      await this.initialize();
    }

    // Convert file to image element for canvas processing
    const imageUrl = URL.createObjectURL(imageFile);
    const img = new Image();
    
    return new Promise((resolve, reject) => {
      img.onload = async () => {
        try {
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d')!;
          
          canvas.width = img.width;
          canvas.height = img.height;
          ctx.drawImage(img, 0, 0);

          // Extract text from each region
          const results: Partial<OpusMagnumData> = {};

          for (const [key, region] of Object.entries(this.regions)) {
            const regionCanvas = document.createElement('canvas');
            const regionCtx = regionCanvas.getContext('2d')!;
            
            regionCanvas.width = region.width;
            regionCanvas.height = region.height;
            
            // Extract region from main image
            regionCtx.drawImage(
              canvas,
              region.x, region.y, region.width, region.height,
              0, 0, region.width, region.height
            );

            // Convert to grayscale and invert (similar to main.py processing)
            const imageData = regionCtx.getImageData(0, 0, region.width, region.height);
            this.preprocessImage(imageData);
            regionCtx.putImageData(imageData, 0, 0);

            // Configure OCR based on content type
            if (key === 'cost') {
              // Cost field has digits + 'G' for gold (content type: 'digits_with_6')
              await this.worker!.setParameters({
                tessedit_char_whitelist: '0123456789G'
              });
            } else if (key === 'cycles' || key === 'area') {
              // Pure digits (content type: 'digits')
              await this.worker!.setParameters({
                tessedit_char_whitelist: '0123456789'
              });
            } else if (key === 'puzzle') {
              // Puzzle name - allow alphanumeric, spaces, and dashes
              await this.worker!.setParameters({
                tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -'
              });
            } else {
              // Default - allow all characters
              await this.worker!.setParameters({
                tessedit_char_whitelist: ''
              });
            }

            // Perform OCR on the region
            const { data: { text } } = await this.worker!.recognize(regionCanvas);
            let cleanText = text.trim();

            // Post-process based on field type
            if (key === 'cost') {
              // Handle common OCR misreadings where G is read as 6
              // If the text ends with 6 and looks like it should be G, remove it
              if (cleanText.endsWith('6') && cleanText.length > 1) {
                // Check if removing the last character gives a reasonable cost value
                const withoutLast = cleanText.slice(0, -1);
                if (/^\d+$/.test(withoutLast)) {
                  cleanText = withoutLast;
                }
              }
              // Remove any trailing G characters
              cleanText = cleanText.replace(/G+$/g, '');
              // Ensure only digits remain
              cleanText = cleanText.replace(/[^0-9]/g, '');
            } else if (key === 'cycles' || key === 'area') {
              // Ensure only digits remain
              cleanText = cleanText.replace(/[^0-9]/g, '');
            } else if (key === 'puzzle') {
              // Post-process puzzle names with fuzzy matching
              cleanText = this.findBestPuzzleMatch(cleanText);
            }

            results[key as keyof OpusMagnumData] = cleanText;
          }

          URL.revokeObjectURL(imageUrl);
          
          resolve({
            puzzle: results.puzzle || '',
            cost: results.cost || '',
            cycles: results.cycles || '',
            area: results.area || ''
          });
        } catch (error) {
          URL.revokeObjectURL(imageUrl);
          reject(error);
        }
      };

      img.onerror = () => {
        URL.revokeObjectURL(imageUrl);
        reject(new Error('Failed to load image'));
      };

      img.src = imageUrl;
    });
  }

  private preprocessImage(imageData: ImageData): void {
    // Convert to grayscale and invert (similar to cv2.bitwise_not in main.py)
    const data = imageData.data;
    
    for (let i = 0; i < data.length; i += 4) {
      // Convert to grayscale
      const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
      
      // Invert the grayscale value
      const inverted = 255 - gray;
      
      data[i] = inverted;     // Red
      data[i + 1] = inverted; // Green
      data[i + 2] = inverted; // Blue
      // Alpha channel (data[i + 3]) remains unchanged
    }
  }

  /**
   * Calculate Levenshtein distance between two strings
   */
  private levenshteinDistance(str1: string, str2: string): number {
    const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
    
    for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
    for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
    
    for (let j = 1; j <= str2.length; j++) {
      for (let i = 1; i <= str1.length; i++) {
        const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
        matrix[j][i] = Math.min(
          matrix[j][i - 1] + 1,     // deletion
          matrix[j - 1][i] + 1,     // insertion
          matrix[j - 1][i - 1] + indicator // substitution
        );
      }
    }
    
    return matrix[str2.length][str1.length];
  }

  /**
   * Find the best matching puzzle name from available options
   */
  private findBestPuzzleMatch(ocrText: string): string {
    if (!this.availablePuzzleNames.length) {
      return ocrText.trim();
    }

    const cleanedOcr = ocrText.trim();
    
    // First try exact match (case insensitive)
    const exactMatch = this.availablePuzzleNames.find(
      name => name.toLowerCase() === cleanedOcr.toLowerCase()
    );
    if (exactMatch) return exactMatch;

    // Then try fuzzy matching
    let bestMatch = cleanedOcr;
    let bestScore = Infinity;
    
    for (const puzzleName of this.availablePuzzleNames) {
      // Calculate similarity scores
      const distance = this.levenshteinDistance(
        cleanedOcr.toLowerCase(), 
        puzzleName.toLowerCase()
      );
      
      // Normalize by length to get a similarity ratio
      const maxLength = Math.max(cleanedOcr.length, puzzleName.length);
      const similarity = 1 - (distance / maxLength);
      
      // Consider it a good match if similarity is above 70%
      if (similarity > 0.7 && distance < bestScore) {
        bestScore = distance;
        bestMatch = puzzleName;
      }
    }
    
    return bestMatch;
  }


  async terminate(): Promise<void> {
    if (this.worker) {
      await this.worker.terminate();
      this.worker = null;
    }
  }

  // Utility method to validate if an image looks like an Opus Magnum screenshot
  static isValidOpusMagnumImage(file: File): boolean {
    // Basic validation - could be enhanced with actual image analysis
    const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
    return validTypes.includes(file.type);
  }

  // Debug method to visualize OCR regions (similar to main.py debug rectangles)
  static drawDebugRegions(imageFile: File): Promise<string> {
    return new Promise((resolve, reject) => {
      const imageUrl = URL.createObjectURL(imageFile);
      const img = new Image();
      
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d')!;
        
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        // Draw debug rectangles
        ctx.strokeStyle = '#00ff00';
        ctx.lineWidth = 2;
        
        const service = new OpusMagnumOCRService();
        Object.values(service.regions).forEach(region => {
          ctx.strokeRect(region.x, region.y, region.width, region.height);
        });

        URL.revokeObjectURL(imageUrl);
        resolve(canvas.toDataURL());
      };

      img.onerror = () => {
        URL.revokeObjectURL(imageUrl);
        reject(new Error('Failed to load image for debug'));
      };

      img.src = imageUrl;
    });
  }
}

// Singleton instance for the application
export const ocrService = new OpusMagnumOCRService();
