"use strict";

var childProcess = require("node:child_process");
var crypto = require("node:crypto");
var fs = require("node:fs");
var os = require("node:os");
var path = require("node:path");

var DEFAULT_PIXELS = " .,:;i1tfLCG08@";

function isObject(value) {
  return value !== null && typeof value === "object" && !Buffer.isBuffer(value);
}

function clamp(value, min, max) {
  var n = Number(value);
  if (!Number.isFinite(n)) return min;
  n = Math.floor(n);
  return Math.max(min, Math.min(max, n));
}

function clampByte(value, fallback) {
  var n = Number(value);
  if (!Number.isFinite(n)) return fallback;
  return clamp(n, 0, 255);
}

function normalizeRgb(value, fallback) {
  if (!isObject(value)) return fallback || null;
  return {
    r: clampByte(value.r, fallback ? fallback.r : 0),
    g: clampByte(value.g, fallback ? fallback.g : 0),
    b: clampByte(value.b, fallback ? fallback.b : 0)
  };
}

function rgbToFfmpegColor(rgb) {
  if (!rgb) return "black";
  function hex(value) {
    return clampByte(value, 0).toString(16).padStart(2, "0");
  }
  return "0x" + hex(rgb.r) + hex(rgb.g) + hex(rgb.b);
}

function luminance(r, g, b) {
  return clampByte(Math.round((0.2126 * r) + (0.7152 * g) + (0.0722 * b)), 0);
}

function percentOrNumber(value, basis, fallback) {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string") {
    var trimmed = value.trim();
    if (trimmed.endsWith("%")) {
      var pct = Number(trimmed.slice(0, -1));
      if (Number.isFinite(pct)) return Math.max(1, Math.floor((basis * pct) / 100));
    }
    var parsed = Number(trimmed);
    if (Number.isFinite(parsed)) return parsed;
  }
  return fallback;
}

function resolveSize(options) {
  var size = isObject(options.size) ? options.size : {};
  var sizeOptions = isObject(options.size_options) ? options.size_options : {};
  var screen = isObject(sizeOptions.screen_size) ? sizeOptions.screen_size : {};
  var screenWidth = clamp(screen.width || process.stdout.columns || 80, 20, 240);
  var screenHeight = clamp(screen.height || process.stdout.rows || 32, 8, 120);
  var width = percentOrNumber(size.width, screenWidth, Math.min(80, screenWidth));
  var height = percentOrNumber(size.height, screenHeight, Math.max(8, Math.round(width * 0.42)));
  return {
    width: clamp(width, 8, 240),
    height: clamp(height, 4, 120)
  };
}

function normalizeOptions(options) {
  if (!isObject(options)) options = {};
  var pixels = options.pixels;
  if (Array.isArray(pixels)) pixels = pixels.join("");
  if (typeof pixels !== "string" || pixels.length < 2) pixels = DEFAULT_PIXELS;
  if (options.reverse === true) pixels = pixels.split("").reverse().join("");
  var pxBackground = null;
  if (options.white_bg === true) pxBackground = { r: 255, g: 255, b: 255 };
  if (isObject(options.px_background)) pxBackground = normalizeRgb(options.px_background, pxBackground);
  return {
    pixels: pixels,
    colored: options.colored === true,
    colorBackground: options.bg === true,
    pxBackground: pxBackground,
    concat: options.concat !== false,
    stringify: options.stringify !== false,
    timeoutMs: clamp(options.timeoutMs || options.timeout || 5000, 500, 60000),
    preserveAspectRatio: !options.size_options || options.size_options.preserve_aspect_ratio !== false,
    size: resolveSize(options),
    stringifyFn: typeof options.stringify_fn === "function" ? options.stringify_fn : null
  };
}

function tempFilePath(ext) {
  var suffix = ext || ".img";
  if (!suffix.startsWith(".")) suffix = "." + suffix;
  return path.join(os.tmpdir(), "omnius-image-to-ascii-" + crypto.randomUUID() + suffix);
}

function extensionFromUrl(url) {
  try {
    var parsed = new URL(url);
    var ext = path.extname(parsed.pathname || "");
    return ext || ".img";
  } catch (e) {
    return ".img";
  }
}

function sourceToFile(source, callback) {
  if (Buffer.isBuffer(source)) {
    var bufPath = tempFilePath(".img");
    fs.writeFile(bufPath, source, function (err) {
      callback(err, bufPath, true);
    });
    return;
  }

  if (typeof source !== "string" || source.trim().length === 0) {
    callback(new Error("image-to-ascii source must be a file path, URL, or Buffer"));
    return;
  }

  if (/^https?:\/\//i.test(source)) {
    if (typeof fetch !== "function") {
      callback(new Error("image-to-ascii URL input requires Node fetch support"));
      return;
    }
    fetch(source).then(function (res) {
      if (!res.ok) throw new Error("image-to-ascii failed to fetch image: HTTP " + res.status);
      return res.arrayBuffer();
    }).then(function (arrayBuffer) {
      var urlPath = tempFilePath(extensionFromUrl(source));
      fs.writeFile(urlPath, Buffer.from(arrayBuffer), function (err) {
        callback(err, urlPath, true);
      });
    }).catch(function (err) {
      callback(err);
    });
    return;
  }

  callback(null, source, false);
}

function buildFilter(width, height, preserveAspectRatio, pxBackground) {
  if (preserveAspectRatio) {
    var padColor = rgbToFfmpegColor(pxBackground);
    return [
      "scale=" + width + ":" + height + ":force_original_aspect_ratio=decrease",
      "pad=" + width + ":" + height + ":(ow-iw)/2:(oh-ih)/2:color=" + padColor,
      "format=rgba"
    ].join(",");
  }
  return "scale=" + width + ":" + height + ",format=rgba";
}

function matrixFromRaw(raw, width, height, pxBackground) {
  var matrix = [];
  for (var y = 0; y < height; y++) {
    var row = [];
    for (var x = 0; x < width; x++) {
      var offset = ((y * width) + x) * 4;
      var r = raw[offset] || 0;
      var g = raw[offset + 1] || 0;
      var b = raw[offset + 2] || 0;
      var a = raw[offset + 3];
      if (a == null) a = 255;
      if (pxBackground && a < 255) {
        var alpha = a / 255;
        r = Math.round((r * alpha) + (pxBackground.r * (1 - alpha)));
        g = Math.round((g * alpha) + (pxBackground.g * (1 - alpha)));
        b = Math.round((b * alpha) + (pxBackground.b * (1 - alpha)));
        a = 255;
      }
      var value = luminance(r, g, b);
      row.push({ r: r, g: g, b: b, a: a, value: value });
    }
    matrix.push(row);
  }
  return matrix;
}

function stringifyMatrix(matrix, options) {
  var lines = [];
  var pixels = options.pixels;
  var maxIdx = pixels.length - 1;
  for (var y = 0; y < matrix.length; y++) {
    var line = "";
    for (var x = 0; x < matrix[y].length; x++) {
      var pixel = matrix[y][x] || {};
      var value = pixel.value || 0;
      var idx = Math.round((value / 255) * maxIdx);
      var ch = pixels[idx] || " ";
      if (options.colored) {
        var r = pixel.r == null ? value : pixel.r;
        var g = pixel.g == null ? value : pixel.g;
        var b = pixel.b == null ? value : pixel.b;
        var mode = options.colorBackground ? "48" : "38";
        line += "\x1b[" + mode + ";2;" + r + ";" + g + ";" + b + "m" + ch + "\x1b[0m";
      } else {
        line += ch;
      }
    }
    lines.push(line.replace(/\s+$/g, ""));
  }
  return options.concat ? lines.join("\n") : lines;
}

function renderFile(filePath, options, callback) {
  var width = options.size.width;
  var height = options.size.height;
  var args = [
    "-hide_banner",
    "-loglevel",
    "error",
    "-i",
    filePath,
    "-vf",
    buildFilter(width, height, options.preserveAspectRatio, options.pxBackground),
    "-frames:v",
    "1",
    "-f",
    "rawvideo",
    "-pix_fmt",
    "rgba",
    "-"
  ];

  childProcess.execFile("ffmpeg", args, {
    encoding: "buffer",
    maxBuffer: (width * height * 4) + 4096,
    timeout: options.timeoutMs
  }, function (err, stdout, stderr) {
    if (err) {
      var detail = stderr && stderr.length ? String(stderr).trim() : err.message;
      callback(new Error("image-to-ascii failed via ffmpeg: " + detail));
      return;
    }
    if (!stdout || stdout.length < width * height * 4) {
      callback(new Error("image-to-ascii decoded too few pixels for " + width + "x" + height));
      return;
    }
    var matrix = matrixFromRaw(stdout, width, height, options.pxBackground);
    if (!options.stringify) {
      callback(null, matrix);
      return;
    }
    if (options.stringifyFn) {
      try {
        callback(null, options.stringifyFn(matrix, options));
      } catch (stringifyErr) {
        callback(stringifyErr);
      }
      return;
    }
    callback(null, stringifyMatrix(matrix, options));
  });
}

function imageToAscii(source, options, callback) {
  if (typeof options === "function") {
    callback = options;
    options = {};
  }
  if (typeof callback !== "function") {
    return new Promise(function (resolve, reject) {
      imageToAscii(source, options || {}, function (err, converted) {
        if (err) reject(err);
        else resolve(converted);
      });
    });
  }

  var normalized = normalizeOptions(options || {});
  sourceToFile(source, function (sourceErr, filePath, cleanup) {
    if (sourceErr) {
      callback(sourceErr);
      return;
    }
    renderFile(filePath, normalized, function (renderErr, converted) {
      if (cleanup) {
        fs.unlink(filePath, function () {});
      }
      callback(renderErr, converted);
    });
  });
}

imageToAscii.defaults = {
  size_options: {
    px_size: { width: 1 }
  },
  stringify: true,
  concat: true,
  size: {
    height: "100%"
  }
};

module.exports = imageToAscii;
module.exports.default = imageToAscii;
