import { ref } from 'vue';
import extract from 'png-chunks-extract';
import encode from 'png-chunks-encode';
import text from 'png-chunk-text';

/**
 * MIT License - Copyright (c) 2021 Kaiido
 *
 * A monkey-patch for Safari's drawImage.
 *
 * This browser doesn't handle well using the cropping abilities of drawImage
 * with out-of-bounds values.
 * (see https://stackoverflow.com/questions/35500999/cropping-with-drawimage-not-working-in-safari)
 * This script takes care of detecting when the monkey-patch is needed,
 * and does redefine the cropping parameters so they fall inside the source's boundaries.
 *
 **/

var patchSafari = () => {
  if (typeof window == 'undefined' || typeof window.CanvasRenderingContext2D == 'undefined') return
  if (!needPoly()) return

  const proto = CanvasRenderingContext2D.prototype;
  const original = proto.drawImage;
  if (!original) {
    console.error('This script requires a basic implementation of drawImage');
    return
  }

  proto.drawImage = function drawImage(source, x, y) {
    // length: 3

    const will_crop = arguments.length === 9;
    if (!will_crop) {
      return original.apply(this, [...arguments])
    }

    const safe_rect = getSafeRect(...arguments);
    if (isEmptyRect(safe_rect)) {
      return
    }
    return original.apply(this, safe_rect)
  };

  function needPoly() {
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.fillRect(0, 0, 40, 40);
    ctx.drawImage(ctx.canvas, -40, -40, 80, 80, 50, 50, 20, 20);

    const img = ctx.getImageData(50, 50, 30, 30); // 10px around expected square
    const data = new Uint32Array(img.data.buffer);
    const colorAt = (x, y) => data[y * img.width + x];

    const transparents = [
      [9, 9],
      [20, 9],
      [9, 20],
      [20, 20],
    ];
    const blacks = [
      [10, 10],
      [19, 10],
      [10, 19],
      [19, 19],
    ];
    return (
      transparents.some(([x, y]) => colorAt(x, y) !== 0x00000000) ||
      blacks.some(([x, y]) => colorAt(x, y) === 0x00000000)
    )
  }

  function getSafeRect(image, sx, sy, sw, sh, dx, dy, dw, dh) {
    const { width, height } = getSourceDimensions(image);

    if (sw < 0) {
      sx += sw;
      sw = Math.abs(sw);
    }
    if (sh < 0) {
      sy += sh;
      sh = Math.abs(sh);
    }
    if (dw < 0) {
      dx += dw;
      dw = Math.abs(dw);
    }
    if (dh < 0) {
      dy += dh;
      dh = Math.abs(dh);
    }
    const x1 = Math.max(sx, 0);
    const x2 = Math.min(sx + sw, width);
    const y1 = Math.max(sy, 0);
    const y2 = Math.min(sy + sh, height);
    const w_ratio = dw / sw;
    const h_ratio = dh / sh;

    return [
      image,
      x1,
      y1,
      x2 - x1,
      y2 - y1,
      sx < 0 ? dx - sx * w_ratio : dx,
      sy < 0 ? dy - sy * h_ratio : dy,
      (x2 - x1) * w_ratio,
      (y2 - y1) * h_ratio,
    ]
  }

  function isEmptyRect(args) {
    // sw, sh, dw, dh
    return [3, 4, 7, 8].some((index) => !args[index])
  }

  function getSourceDimensions(source) {
    const sourceIs = (type) => {
      const constructor = globalThis[type];
      return constructor && source instanceof constructor
    };
    if (sourceIs('HTMLImageElement')) {
      return { width: source.naturalWidth, height: source.naturalHeight }
    } else if (sourceIs('HTMLVideoElement')) {
      return { width: source.videoWidth, height: source.videoHeight }
    } else if (sourceIs('SVGImageElement')) {
      throw new TypeError(
        "SVGImageElement isn't yet supported as source image.",
        'UnsupportedError',
      )
    } else if (sourceIs('HTMLCanvasElement') || sourceIs('ImageBitmap')) {
      return source
    }
  }
};

const error = ref(null);

function canvasToBuffer(canvas) {
  try {
    const base64 = canvas.toDataURL('image/png').split(',')[1];
    const bytes = atob(base64);
    const buffer = new Uint8Array(bytes.length);
    for (let i = 0; i < bytes.length; i++) {
      buffer[i] = bytes.charCodeAt(i);
    }
    return buffer
  } catch (e) {
    error.value = 'Failed to convert canvas to buffer: ' + e.message;
    return null
  }
}

function embedInSvg(svgString, data) {
  try {
    const metadata = `<metadata>
      <gun-data>${JSON.stringify(data)}</gun-data>
    </metadata>`;
    return svgString.replace('</svg>', `${metadata}</svg>`)
  } catch (e) {
    error.value = 'Failed to embed data in SVG: ' + e.message;
    return null
  }
}

function extractFromSvg(svgString) {
  try {
    const match = svgString.match(/<gun-data>(.*?)<\/gun-data>/s);
    return match ? JSON.parse(match[1]) : null
  } catch (e) {
    error.value = 'Failed to extract data from SVG: ' + e.message;
    return null
  }
}

function embedInImage(canvas, data, format = 'png') {
  if (format === 'svg') {
    const svgString = canvas.outerHTML || canvas;
    return embedInSvg(svgString, data)
  }
  // Default PNG handling
  try {
    const buffer = canvasToBuffer(canvas);
    if (!buffer) return null
    const chunks = extract(buffer);
    chunks.splice(-1, 0, text.encode('message', JSON.stringify(data)));
    return encode(chunks)
  } catch (e) {
    error.value = 'Failed to embed data: ' + e.message;
    return null
  }
}

function extractFromBuffer(buffer, type = 'image/png') {
  if (type === 'image/svg+xml') {
    const text = new TextDecoder().decode(buffer);
    return extractFromSvg(text)
  }
  // Default PNG handling
  try {
    const chunks = extract(buffer);
    const textChunks = chunks
      .filter(chunk => chunk.name === 'tEXt')
      .map(chunk => text.decode(chunk.data));

    const messageChunk = textChunks.find(chunk => chunk.keyword === 'message');
    return messageChunk ? JSON.parse(messageChunk.text) : null
  } catch (e) {
    error.value = e.message;
    return null
  }
}

// Keep this async function for File inputs only
async function extractFromFile(file) {
  try {
    if (!(file instanceof File)) {
      throw new Error('Input must be a File object')
    }
    const arrayBuffer = await file.arrayBuffer();
    const buffer = new Uint8Array(arrayBuffer);
    return await extractFromBuffer(buffer)
  } catch (e) {
    error.value = e.message;
    return null
  }
}

// https://datatracker.ietf.org/doc/html/rfc4648#section-5
const symbols =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

function fromB64(x) { return x.split("").reduce((s, v) => s * 64 + symbols.indexOf(v), 0) }

function decodeUrlSafeBase64(st) {
  const symbolArray = symbols.split("");
  let arr = [];
  let i = 0;
  for (let letter of st) {
    arr[i++] = symbolArray.indexOf(letter) / 64;
  }
  return arr;
}

const cache = {};

/**
 * Generate avatar from public key
 */
function gunAvatar({
  pub,
  size = 200,
  dark = false,
  draw = "circles",
  reflect = true,
  round = true,
  embed = true,
}) {
  const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
  if (!validatePub(pub)) return '';
  if (!isBrowser) return createFallbackSVG({ pub, size, dark, embed });

  const key = JSON.stringify(arguments[0]);
  if (cache?.[key]) return cache[key]

  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext("2d");

  const { decoded, finals } = parsePub(pub);

  drawGradient({ ctx, top: finals[0], bottom: finals[1], size, dark });

  if (draw == "squares") {
    ctx.filter = "blur(20px)";
    drawSquares(decoded[0], ctx, size);
    ctx.filter = "blur(0px)";
    ctx.globalCompositeOperation = "color-burn";
    drawSquares(decoded[1], ctx, size);
  } else {
    drawCircles(decoded[0], ctx, size, 0.42 * size);
    ctx.globalCompositeOperation = "multiply";
    drawCircles(decoded[1], ctx, size, 0.125 * size);
  }

  if (reflect) {
    ctx.globalCompositeOperation = "source-over";
    ctx.scale(-1, 1);
    ctx.translate(-size / 2, 0);
    ctx.drawImage(canvas, size / 2, 0, size, size, 0, 0, size, size);
    // Reset transformation matrix after reflection
    ctx.setTransform(1, 0, 0, 1, 0, 0);
  }

  if (round) {
    // Store the current canvas content
    const imageData = ctx.getImageData(0, 0, size, size);
    ctx.clearRect(0, 0, size, size);

    // Fill with background color first
    ctx.fillStyle = dark ? '#cccccc' : '#ffffff';
    ctx.fillRect(0, 0, size, size);

    // Draw original image back
    ctx.putImageData(imageData, 0, 0);

    // Create circular mask
    ctx.globalCompositeOperation = 'destination-in';
    ctx.beginPath();
    ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
    ctx.closePath();
    ctx.fill();
  }

  let image = canvas.toDataURL("image/png");

  if (embed) {
    const embedData = {
      pub,
      content: embed
    };
    const embedBuffer = embedInImage(canvas, embedData);
    if (embedBuffer) {
      const blob = new Blob([embedBuffer], { type: 'image/png' });
      image = URL.createObjectURL(blob);
    }
  }

  cache[key] = image;
  return image;
}

function validatePub(pub) {
  return pub && typeof pub === 'string' && pub.length === 87 && pub.split('.').length === 2;
}

function parsePub(pub) {
  const split = pub.split(".");
  const decoded = split.map(single => decodeUrlSafeBase64(single));
  const finals = decoded.map(d => d[42]);
  const averages = decoded.map(e => e.reduce((acc, d) => acc + d) / e.length);
  const angles = split.map(part => fromB64(part) % 360);
  const colors = split.map((s, i) => `hsl(${angles[i]} ${finals[i] * 100}% ${averages[i] * 100}%)`);
  return { finals, decoded, angles, averages, colors }
}

function chunkIt(list, chunkSize = 3) {
  return [...Array(Math.ceil(list.length / chunkSize))].map(() =>
    list.splice(0, chunkSize)
  );
}

function drawGradient({ ctx, top = 0, bottom = 150, size = 200, dark = false }) {
  const gradient = ctx.createLinearGradient(0, 0, 0, size);
  const offset = dark ? 0 : 70;
  gradient.addColorStop(0, `hsla(0,0%,${offset + top * 30}%)`);
  gradient.addColorStop(1, `hsla(0,0%,${offset + bottom * 30}%)`);
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, size, size);
}

function drawSquares(data, ctx, size) {
  chunkIt(data, 14).forEach(chunk => {
    if (chunk.length === 14) {
      let [x, y, rRaw, h1, s1, l1, a1, x1, h2, s2, l2, a2, x2, angle] = chunk;
      let r = size / 8 + rRaw * size * (7 / 8);
      const gradient = ctx.createLinearGradient(
        x * size + r * x1, 0,
        x * size + r * x2, size
      );
      gradient.addColorStop(0, `hsla(${h1 * 360},${s1 * 100}%,${l1 * 100}%,${a1})`);
      gradient.addColorStop(1, `hsla(${h2 * 360},${s2 * 100}%,${l2 * 100}%,${a2})`);
      ctx.fillStyle = gradient;
      ctx.translate(x * size, y * size);
      ctx.rotate(angle * Math.PI);
      ctx.fillRect(-r / 2, -r / 2, r, r);
      ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
  });
}

function drawCircles(data, ctx, size, radius) {
  chunkIt(data, 7).forEach(chunk => {
    if (chunk.length === 7) {
      let [x, y, r, h, s, l, a] = chunk;
      ctx.beginPath();
      ctx.arc(
        size / 2 + (x * size) / 2,
        y * size,
        r * radius,
        0,
        2 * Math.PI
      );
      ctx.fillStyle = `hsla(${h * 360},${s * 100}%,${l * 100}%,${a})`;
      ctx.closePath();
      ctx.fill();
    }
  });
}

// ====
//  Fallback SVG for SSR
// ====

function createFallbackSVG({ pub, size = 200, dark = false, embed = true } = {}) {
  const { decoded, finals } = parsePub(pub);

  // Create gradient background
  const bgGradient = `
    <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="hsla(0,0%,${(dark ? 0 : 70) + finals[0] * 30}%)"/>
      <stop offset="100%" stop-color="hsla(0,0%,${(dark ? 0 : 70) + finals[1] * 30}%)"/>
    </linearGradient>
  `;

  // Generate circles for both layers
  const createCircles = (data, radius, isSecond = false) => {
    return chunkIt(data, 7).map(chunk => {
      if (chunk.length !== 7) return '';
      const [x, y, r, h, s, l, a] = chunk;
      const cx = size / 2 + (x * size) / 2;
      const cy = y * size;
      const rad = r * radius;
      return `
        <circle 
          cx="${cx}" cy="${cy}" r="${rad}"
          fill="hsla(${h * 360},${s * 100}%,${l * 100}%,${a})"
          style="${isSecond ? 'mix-blend-mode:multiply;' : ''}"
        />
        <circle 
          cx="${size - cx}" cy="${cy}" r="${rad}"
          fill="hsla(${h * 360},${s * 100}%,${l * 100}%,${a})"
          style="${isSecond ? 'mix-blend-mode:multiply;' : ''}"
        />
      `;
    }).join('');
  };

  let svg = `
    <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
      <defs>${bgGradient}</defs>
      <rect width="${size}" height="${size}" fill="url(#bg)"/>
      ${createCircles(decoded[0], 0.42 * size)}
      ${createCircles(decoded[1], 0.125 * size, true)}
    </svg>
  `;
  if (embed) {
    const embedData = { pub, };
    if (embed && embed == true) { embedData.content = embed; }
    svg = embedInImage(svg, embedData, 'svg');
  }

  return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
}

function mountClass(elClass = "gun-avatar") {
  document.addEventListener("DOMContentLoaded", () => {
    let avatars = document.getElementsByClassName(elClass);
    for (let i in avatars) {
      const img = avatars[i];
      if (img.dataset.round !== "false") {
        img.style.borderRadius = "100%";
      }

      let embed = img.dataset.embed;
      if (img.dataset.embed !== 'true') {
        try {
          embed = JSON.parse(img.dataset.embed);
        } catch (e) {
          console.warn('Invalid content JSON in data-embed attribute');
        }
      }

      img.src = gunAvatar({
        pub: img.dataset.pub,
        size: Number(img.dataset.size),
        dark: Boolean(img.dataset.dark),
        draw: img.dataset.draw,
        reflect: img.dataset.reflect !== "false",
        embed: embed === "false" ? false : embed || true
      });
    }
  });
}

function mountElement(elName = "gun-avatar") {
  let initiated = false;
  if (initiated) return;

  class Avatar extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
      /** @type {HTMLImageElement} */
      this.img = document.createElement("img");
      this.shadowRoot.append(this.img);
    }

    render() {
      this.pub = this.getAttribute("pub") || "1234123455Ute2tFhdjDQgzR-1234lfSlZxgEZKuquI.2F-j1234434U1234Asj-5lxnECG5TDyuPD8gEiuI123";
      this.size = this.hasAttribute("size") ? Number(this.getAttribute("size")) : 400;
      this.draw = this.getAttribute("draw") || "circles";
      this.reflect = this.hasAttribute("reflect") ? this.getAttribute("reflect") !== "false" : true;
      this.round = this.hasAttribute("round") || this.getAttribute("round") === "";
      this.dark = this.hasAttribute("dark") || this.getAttribute("dark") === "";
      this.embed = this.hasAttribute("embed") ? this.getAttribute("embed") !== "false" : true;

      let embed = this.getAttribute("embed");
      if (this.getAttribute("embed")) {
        try {
          embed = JSON.parse(this.getAttribute("embed"));
        } catch (e) {
          console.warn('Invalid content JSON in embed attribute');
        }
      }

      this.img.style.borderRadius = this.round ? "100%" : "0%";

      this.img.src = gunAvatar({
        pub: this.pub,
        size: this.size,
        dark: this.dark,
        draw: this.draw,
        reflect: this.reflect,
        embed: embed === "false" ? false : embed || true
      });
    }

    connectedCallback() {
      this.render();
    }

    static get observedAttributes() {
      return ["pub", "round", "size", "dark", "draw", "reflect", "embed"];
    }

    attributeChangedCallback() {
      this.render();
    }
  }

  customElements.define(elName, Avatar);
  initiated = true;
}

patchSafari();

export { embedInImage, error, extractFromFile, gunAvatar, mountClass, mountElement, parsePub };
