import { Injectable } from '@angular/core';
import {
  PROFILE_AVATAR_ALLOWED_MIME_TYPES,
  PROFILE_AVATAR_OUTPUT_SIZE,
  ProfileAvatarTransform,
  EditableProfileAvatarSource,
  ProcessedProfileAvatar,
  clampProfileAvatarTransform,
  PROFILE_AVATAR_EDITOR_FRAME_SIZE,
  resolveProfileAvatarBaseScale
} from '../../domain/profile-avatar.models';

const PROFILE_AVATAR_OUTPUT_MIME = 'image/webp';
const PROFILE_AVATAR_OUTPUT_QUALITY = 0.92;

export function isAnimatedGif(buffer: ArrayBuffer): boolean {
  const bytes = new Uint8Array(buffer);

  if (bytes.length < 13 || readAscii(bytes, 0, 6) !== 'GIF87a' && readAscii(bytes, 0, 6) !== 'GIF89a') {
    return false;
  }

  let offset = 13;

  if ((bytes[10] & 0x80) !== 0) {
    offset += 3 * (2 ** ((bytes[10] & 0x07) + 1));
  }

  let frameCount = 0;

  while (offset < bytes.length) {
    const blockType = bytes[offset];

    if (blockType === 0x3B) {
      return false;
    }

    if (blockType === 0x21) {
      offset += 2;

      while (offset < bytes.length) {
        const blockSize = bytes[offset++];

        if (blockSize === 0) {
          break;
        }

        offset += blockSize;
      }

      continue;
    }

    if (blockType !== 0x2C || offset + 10 > bytes.length) {
      return false;
    }

    frameCount++;

    if (frameCount > 1) {
      return true;
    }

    const packedFields = bytes[offset + 9];

    offset += 10;

    if ((packedFields & 0x80) !== 0) {
      offset += 3 * (2 ** ((packedFields & 0x07) + 1));
    }

    offset += 1;

    while (offset < bytes.length) {
      const blockSize = bytes[offset++];

      if (blockSize === 0) {
        break;
      }

      offset += blockSize;
    }
  }

  return false;
}

export function isAnimatedWebp(buffer: ArrayBuffer): boolean {
  const bytes = new Uint8Array(buffer);

  if (bytes.length < 16 || readAscii(bytes, 0, 4) !== 'RIFF' || readAscii(bytes, 8, 4) !== 'WEBP') {
    return false;
  }

  let offset = 12;

  while (offset + 8 <= bytes.length) {
    const chunkType = readAscii(bytes, offset, 4);
    const chunkSize = readUint32LittleEndian(bytes, offset + 4);

    if (chunkType === 'ANIM' || chunkType === 'ANMF') {
      return true;
    }

    if (chunkType === 'VP8X' && offset + 9 <= bytes.length) {
      const featureFlags = bytes[offset + 8];

      if ((featureFlags & 0x02) !== 0) {
        return true;
      }
    }

    offset += 8 + chunkSize + (chunkSize % 2);
  }

  return false;
}

@Injectable({ providedIn: 'root' })
export class ProfileAvatarImageService {
  validateFile(file: File): string | null {
    const mimeType = file.type.toLowerCase();
    const normalizedName = file.name.toLowerCase();
    const isAllowedMime = PROFILE_AVATAR_ALLOWED_MIME_TYPES.includes(mimeType as typeof PROFILE_AVATAR_ALLOWED_MIME_TYPES[number]);
    const isAllowedExtension = normalizedName.endsWith('.webp')
      || normalizedName.endsWith('.gif')
      || normalizedName.endsWith('.jpg')
      || normalizedName.endsWith('.jpeg');

    if (!isAllowedExtension || (mimeType && !isAllowedMime)) {
      return 'Invalid file type. Use WebP, GIF, JPG, or JPEG.';
    }

    return null;
  }

  async prepareEditableSource(file: File): Promise<EditableProfileAvatarSource> {
    const objectUrl = URL.createObjectURL(file);
    const mime = this.resolveSourceMime(file);

    try {
      const [image, preservesAnimation] = await Promise.all([this.loadImage(objectUrl), this.detectAnimatedSource(file, mime)]);

      return {
        file,
        objectUrl,
        mime,
        name: file.name,
        width: image.naturalWidth,
        height: image.naturalHeight,
        preservesAnimation
      };
    } catch (error) {
      URL.revokeObjectURL(objectUrl);
      throw error;
    }
  }

  releaseEditableSource(source: EditableProfileAvatarSource | null | undefined): void {
    if (!source?.objectUrl) {
      return;
    }

    URL.revokeObjectURL(source.objectUrl);
  }

  async processEditableSource(
    source: EditableProfileAvatarSource,
    transform: ProfileAvatarTransform
  ): Promise<ProcessedProfileAvatar> {
    if (source.preservesAnimation) {
      return this.processAnimatedSource(source);
    }

    const image = await this.loadImage(source.objectUrl);
    const canvas = document.createElement('canvas');

    canvas.width = PROFILE_AVATAR_OUTPUT_SIZE;
    canvas.height = PROFILE_AVATAR_OUTPUT_SIZE;

    const context = canvas.getContext('2d');

    if (!context) {
      throw new Error('Canvas not supported');
    }

    const clampedTransform = clampProfileAvatarTransform(source, transform);
    const previewScale = resolveProfileAvatarBaseScale(source, PROFILE_AVATAR_EDITOR_FRAME_SIZE) * clampedTransform.zoom;
    const renderRatio = PROFILE_AVATAR_OUTPUT_SIZE / PROFILE_AVATAR_EDITOR_FRAME_SIZE;
    const drawWidth = image.naturalWidth * previewScale * renderRatio;
    const drawHeight = image.naturalHeight * previewScale * renderRatio;
    const drawX = (PROFILE_AVATAR_OUTPUT_SIZE - drawWidth) / 2 + clampedTransform.offsetX * renderRatio;
    const drawY = (PROFILE_AVATAR_OUTPUT_SIZE - drawHeight) / 2 + clampedTransform.offsetY * renderRatio;

    context.clearRect(0, 0, canvas.width, canvas.height);
    context.imageSmoothingEnabled = true;
    context.imageSmoothingQuality = 'high';
    context.drawImage(image, drawX, drawY, drawWidth, drawHeight);

    const renderedBlob = await this.canvasToBlob(canvas, PROFILE_AVATAR_OUTPUT_MIME, PROFILE_AVATAR_OUTPUT_QUALITY);
    const compressedBlob = renderedBlob;
    const updatedAt = Date.now();
    const dataUrl = await this.readBlobAsDataUrl(compressedBlob);
    const hash = await this.computeHash(compressedBlob);

    return {
      blob: compressedBlob,
      base64: dataUrl.split(',', 2)[1] ?? '',
      avatarUrl: dataUrl,
      avatarHash: hash,
      avatarMime: compressedBlob.type || PROFILE_AVATAR_OUTPUT_MIME,
      avatarUpdatedAt: updatedAt,
      width: PROFILE_AVATAR_OUTPUT_SIZE,
      height: PROFILE_AVATAR_OUTPUT_SIZE
    };
  }

  private async processAnimatedSource(source: EditableProfileAvatarSource): Promise<ProcessedProfileAvatar> {
    const updatedAt = Date.now();
    const dataUrl = await this.readBlobAsDataUrl(source.file);
    const hash = await this.computeHash(source.file);

    return {
      blob: source.file,
      base64: dataUrl.split(',', 2)[1] ?? '',
      avatarUrl: dataUrl,
      avatarHash: hash,
      avatarMime: source.mime || source.file.type || PROFILE_AVATAR_OUTPUT_MIME,
      avatarUpdatedAt: updatedAt,
      width: source.width,
      height: source.height
    };
  }

  private async detectAnimatedSource(file: File, mime: string): Promise<boolean> {
    if (mime !== 'image/gif' && mime !== 'image/webp') {
      return false;
    }

    const buffer = await file.arrayBuffer();

    return mime === 'image/gif'
      ? isAnimatedGif(buffer)
      : isAnimatedWebp(buffer);
  }

  private resolveSourceMime(file: File): string {
    const mimeType = file.type.toLowerCase();

    if (mimeType === 'image/jpg') {
      return 'image/jpeg';
    }

    if (mimeType) {
      return mimeType;
    }

    const normalizedName = file.name.toLowerCase();

    if (normalizedName.endsWith('.gif')) {
      return 'image/gif';
    }

    if (normalizedName.endsWith('.jpg') || normalizedName.endsWith('.jpeg')) {
      return 'image/jpeg';
    }

    if (normalizedName.endsWith('.webp')) {
      return 'image/webp';
    }

    return PROFILE_AVATAR_OUTPUT_MIME;
  }

  private async computeHash(blob: Blob): Promise<string> {
    const buffer = await blob.arrayBuffer();
    const digest = await crypto.subtle.digest('SHA-256', buffer);

    return Array.from(new Uint8Array(digest))
      .map((value) => value.toString(16).padStart(2, '0'))
      .join('');
  }

  private canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number): Promise<Blob> {
    return new Promise((resolve, reject) => {
      canvas.toBlob((blob) => {
        if (blob) {
          resolve(blob);
          return;
        }

        reject(new Error('Failed to render avatar image'));
      }, type, quality);
    });
  }

  private readBlobAsDataUrl(blob: Blob): Promise<string> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => {
        if (typeof reader.result === 'string') {
          resolve(reader.result);
          return;
        }

        reject(new Error('Failed to encode avatar image'));
      };

      reader.onerror = () => reject(reader.error ?? new Error('Failed to read avatar image'));
      reader.readAsDataURL(blob);
    });
  }

  private loadImage(url: string): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
      const image = new Image();

      image.onload = () => resolve(image);
      image.onerror = () => reject(new Error('Failed to load avatar image'));
      image.src = url;
    });
  }
}

function readAscii(bytes: Uint8Array, offset: number, length: number): string {
  return String.fromCharCode(...bytes.slice(offset, offset + length));
}

function readUint32LittleEndian(bytes: Uint8Array, offset: number): number {
  return bytes[offset]
    | (bytes[offset + 1] << 8)
    | (bytes[offset + 2] << 16)
    | (bytes[offset + 3] << 24);
}
