/**
 * ==========================================================================
 * HCL COLOR CONVERTER
 * ==========================================================================
 * Accurate color space conversions between RGB, LAB, and HCL
 * Based on CIE LAB color space for perceptual uniformity
 * ==========================================================================
 */

import { RGB, LAB, HCL } from '../models/hcl.models';

export class HCLConverter {
  
  // ==========================================================================
  // RGB ↔ HCL CONVERSIONS
  // ==========================================================================

  /**
   * Convert RGB to HCL color space
   * @param r Red component (0-255)
   * @param g Green component (0-255)
   * @param b Blue component (0-255)
   * @returns HCL color
   */
  static rgbToHcl(r: number, g: number, b: number): HCL {
    const lab = this.rgbToLab(r, g, b);
    return this.labToHcl(lab.l, lab.a, lab.b);
  }

  /**
   * Convert HCL to RGB color space
   * @param h Hue (0-360)
   * @param c Chroma (0-100+)
   * @param l Lightness (0-100)
   * @returns RGB color
   */
  static hclToRgb(h: number, c: number, l: number): RGB {
    const lab = this.hclToLab(h, c, l);
    return this.labToRgb(lab.l, lab.a, lab.b);
  }

  // ==========================================================================
  // HEX CONVERSIONS
  // ==========================================================================

  /**
   * Convert hex string to HCL
   * @param hex Hex color string (#RRGGBB or #RGB)
   * @returns HCL color
   */
  static hexToHcl(hex: string): HCL {
    const rgb = this.hexToRgb(hex);
    return this.rgbToHcl(rgb.r, rgb.g, rgb.b);
  }

  /**
   * Convert HCL to hex string
   * @param h Hue (0-360)
   * @param c Chroma (0-100+)
   * @param l Lightness (0-100)
   * @returns Hex color string
   */
  static hclToHex(h: number, c: number, l: number): string {
    const rgb = this.hclToRgb(h, c, l);
    return this.rgbToHex(rgb.r, rgb.g, rgb.b);
  }

  // ==========================================================================
  // RGB ↔ LAB CONVERSIONS (via XYZ)
  // ==========================================================================

  /**
   * Convert RGB to LAB color space
   * @param r Red component (0-255)
   * @param g Green component (0-255)
   * @param b Blue component (0-255)
   * @returns LAB color
   */
  static rgbToLab(r: number, g: number, b: number): LAB {
    // Convert RGB to XYZ first
    let rNorm = r / 255;
    let gNorm = g / 255;
    let bNorm = b / 255;

    // Apply gamma correction (sRGB)
    rNorm = rNorm > 0.04045 ? Math.pow((rNorm + 0.055) / 1.055, 2.4) : rNorm / 12.92;
    gNorm = gNorm > 0.04045 ? Math.pow((gNorm + 0.055) / 1.055, 2.4) : gNorm / 12.92;
    bNorm = bNorm > 0.04045 ? Math.pow((bNorm + 0.055) / 1.055, 2.4) : bNorm / 12.92;

    // Convert to XYZ using sRGB matrix
    let x = rNorm * 0.4124564 + gNorm * 0.3575761 + bNorm * 0.1804375;
    let y = rNorm * 0.2126729 + gNorm * 0.7151522 + bNorm * 0.0721750;
    let z = rNorm * 0.0193339 + gNorm * 0.1191920 + bNorm * 0.9503041;

    // Normalize by D65 illuminant
    x = x / 0.95047;
    y = y / 1.00000;
    z = z / 1.08883;

    // Convert XYZ to LAB
    const fx = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x + 16/116);
    const fy = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y + 16/116);
    const fz = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z + 16/116);

    const l = (116 * fy) - 16;
    const a = 500 * (fx - fy);
    const bValue = 200 * (fy - fz);

    return { l: Math.max(0, Math.min(100, l)), a, b: bValue };
  }

  /**
   * Convert LAB to RGB color space
   * @param l Lightness (0-100)
   * @param a Green-Red axis
   * @param b Blue-Yellow axis
   * @returns RGB color
   */
  static labToRgb(l: number, a: number, b: number): RGB {
    // Convert LAB to XYZ
    const fy = (l + 16) / 116;
    const fx = a / 500 + fy;
    const fz = fy - b / 200;

    const x = (fx > 0.206897 ? Math.pow(fx, 3) : (fx - 16/116) / 7.787) * 0.95047;
    const y = (fy > 0.206897 ? Math.pow(fy, 3) : (fy - 16/116) / 7.787) * 1.00000;
    const z = (fz > 0.206897 ? Math.pow(fz, 3) : (fz - 16/116) / 7.787) * 1.08883;

    // Convert XYZ to RGB
    let r = x *  3.2404542 + y * -1.5371385 + z * -0.4985314;
    let g = x * -0.9692660 + y *  1.8760108 + z *  0.0415560;
    let b2 = x *  0.0556434 + y * -0.2040259 + z *  1.0572252;

    // Apply inverse gamma correction
    r = r > 0.0031308 ? 1.055 * Math.pow(r, 1/2.4) - 0.055 : 12.92 * r;
    g = g > 0.0031308 ? 1.055 * Math.pow(g, 1/2.4) - 0.055 : 12.92 * g;
    b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1/2.4) - 0.055 : 12.92 * b2;

    // Clamp to valid RGB range
    const rFinal = Math.max(0, Math.min(255, Math.round(r * 255)));
    const gFinal = Math.max(0, Math.min(255, Math.round(g * 255)));
    const bFinal = Math.max(0, Math.min(255, Math.round(b2 * 255)));

    return { r: rFinal, g: gFinal, b: bFinal };
  }

  // ==========================================================================
  // LAB ↔ HCL CONVERSIONS
  // ==========================================================================

  /**
   * Convert LAB to HCL (cylindrical representation)
   * @param l Lightness (0-100)
   * @param a Green-Red axis
   * @param b Blue-Yellow axis
   * @returns HCL color
   */
  static labToHcl(l: number, a: number, b: number): HCL {
    const c = Math.sqrt(a * a + b * b);
    let h = Math.atan2(b, a) * (180 / Math.PI);
    
    // Normalize hue to 0-360
    if (h < 0) h += 360;
    
    return { 
      h: isNaN(h) ? 0 : h,
      c: isNaN(c) ? 0 : c,
      l: Math.max(0, Math.min(100, l))
    };
  }

  /**
   * Convert HCL to LAB color space
   * @param h Hue (0-360)
   * @param c Chroma (0-100+)
   * @param l Lightness (0-100)
   * @returns LAB color
   */
  static hclToLab(h: number, c: number, l: number): LAB {
    const hRad = (h * Math.PI) / 180;
    const a = c * Math.cos(hRad);
    const b = c * Math.sin(hRad);
    
    return { l, a, b };
  }

  // ==========================================================================
  // HEX UTILITY METHODS
  // ==========================================================================

  /**
   * Convert hex string to RGB
   * @param hex Hex color string
   * @returns RGB color
   */
  static hexToRgb(hex: string): RGB {
    // Remove # if present
    hex = hex.replace('#', '');
    
    // Handle 3-digit hex
    if (hex.length === 3) {
      hex = hex.split('').map(h => h + h).join('');
    }
    
    if (hex.length !== 6) {
      throw new Error(`Invalid hex color: ${hex}`);
    }
    
    const r = parseInt(hex.substr(0, 2), 16);
    const g = parseInt(hex.substr(2, 2), 16);
    const b = parseInt(hex.substr(4, 2), 16);
    
    return { r, g, b };
  }

  /**
   * Convert RGB to hex string
   * @param r Red component (0-255)
   * @param g Green component (0-255)
   * @param b Blue component (0-255)
   * @returns Hex color string
   */
  static rgbToHex(r: number, g: number, b: number): string {
    const toHex = (n: number) => {
      const hex = Math.max(0, Math.min(255, Math.round(n))).toString(16);
      return hex.length === 1 ? '0' + hex : hex;
    };
    
    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
  }

  // ==========================================================================
  // COLOR MANIPULATION METHODS
  // ==========================================================================

  /**
   * Adjust lightness of a color
   * @param hex Original color
   * @param amount Lightness adjustment (-100 to 100)
   * @returns Adjusted color
   */
  static adjustLightness(hex: string, amount: number): string {
    const hcl = this.hexToHcl(hex);
    const newL = Math.max(0, Math.min(100, hcl.l + amount));
    return this.hclToHex(hcl.h, hcl.c, newL);
  }

  /**
   * Adjust chroma (saturation) of a color
   * @param hex Original color
   * @param amount Chroma adjustment (-100 to 100)
   * @returns Adjusted color
   */
  static adjustChroma(hex: string, amount: number): string {
    const hcl = this.hexToHcl(hex);
    const newC = Math.max(0, hcl.c + amount);
    return this.hclToHex(hcl.h, newC, hcl.l);
  }

  /**
   * Adjust hue of a color
   * @param hex Original color
   * @param amount Hue adjustment in degrees (-360 to 360)
   * @returns Adjusted color
   */
  static adjustHue(hex: string, amount: number): string {
    const hcl = this.hexToHcl(hex);
    let newH = hcl.h + amount;
    
    // Normalize hue to 0-360
    while (newH < 0) newH += 360;
    while (newH >= 360) newH -= 360;
    
    return this.hclToHex(newH, hcl.c, hcl.l);
  }

  // ==========================================================================
  // VALIDATION METHODS
  // ==========================================================================

  /**
   * Validate if a string is a valid hex color
   * @param hex Color string to validate
   * @returns True if valid hex color
   */
  static isValidHex(hex: string): boolean {
    try {
      const normalizedHex = hex.replace('#', '');
      return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(normalizedHex);
    } catch {
      return false;
    }
  }

  /**
   * Calculate relative luminance of a color (for contrast calculation)
   * @param hex Hex color string
   * @returns Relative luminance (0-1)
   */
  static getRelativeLuminance(hex: string): number {
    const rgb = this.hexToRgb(hex);
    const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
      c = c / 255;
      return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    });
    
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  }

  /**
   * Calculate contrast ratio between two colors
   * @param color1 First color
   * @param color2 Second color
   * @returns Contrast ratio (1-21)
   */
  static getContrastRatio(color1: string, color2: string): number {
    const lum1 = this.getRelativeLuminance(color1);
    const lum2 = this.getRelativeLuminance(color2);
    const lighter = Math.max(lum1, lum2);
    const darker = Math.min(lum1, lum2);
    
    return (lighter + 0.05) / (darker + 0.05);
  }
}