/*!
 * pp-uploader — PPPHP's dependency-free File/Image upload control.
 *
 * Self-enhancing (the same contract as pp-uploader's sibling controls): define
 * window.__ppUploaderInit(root) once; both the Designer preview and the
 * generated app call it to (re)enhance any `.pp-uploader` inside `root`.
 *
 * The control uploads each file by AJAX to its endpoint (api/upload.php), which
 * returns a StorageRef; the refs are kept as JSON in a hidden input named for
 * the field, so the surrounding <form> persists them on submit with NO
 * multipart form and NO server-side $_FILES handling. Large files stream in
 * chunks so shared-hosting upload_max_filesize never caps them.
 *
 * Config (JSON in data-pp-uploader):
 *   endpoint    string  api/upload.php URL ('' = preview/no-op)
 *   field       string  the record column name (also the hidden input name)
 *   connection  string  storage connection id (default 'local')
 *   multi       bool    allow more than one file
 *   maxFiles    int     0 = unlimited
 *   maxSize     int     per-file byte ceiling (0 = unlimited)
 *   accept      string  comma extensions e.g. "jpg,png,pdf" ('' = any non-denied)
 *   imageOnly   bool    restrict to images
 *   chunkSize   int     bytes per chunk (0 disables chunking)
 *   compress    {maxDim:int, quality:0..1}  optional client image downscale
 *   token       string  CSRF token to send with each upload
 */
(function () {
  if (window.__ppUploader) return;
  window.__ppUploader = true;

  var ICON = {
    image: 'M4 5h16v14H4z', // simplified; CSS draws the real glyphs
  };

  function h(tag, cls, html) {
    var e = document.createElement(tag);
    if (cls) e.className = cls;
    if (html != null) e.innerHTML = html;
    return e;
  }
  function bytes(n) {
    if (!n || n < 0) return '';
    var u = ['B', 'KB', 'MB', 'GB', 'TB'], i = Math.floor(Math.log(n) / Math.log(1024));
    i = Math.max(0, Math.min(i, u.length - 1));
    var v = n / Math.pow(1024, i);
    return (i === 0 ? n : v.toFixed(v >= 100 ? 0 : 1)) + ' ' + u[i];
  }
  function extOf(name) { var m = /\.([a-z0-9]+)$/i.exec(name || ''); return m ? m[1].toLowerCase() : ''; }
  function isImgName(name) { return /\.(jpe?g|png|gif|webp|svg|bmp|avif|ico)$/i.test(name || ''); }
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]; }); }

  function fileIconClass(name) {
    var e = extOf(name);
    if (isImgName(name)) return 'ppu-ic-img';
    if (e === 'pdf') return 'ppu-ic-pdf';
    if (['doc', 'docx'].indexOf(e) > -1) return 'ppu-ic-doc';
    if (['xls', 'xlsx', 'csv'].indexOf(e) > -1) return 'ppu-ic-xls';
    if (['zip', 'rar', '7z', 'gz', 'tar'].indexOf(e) > -1) return 'ppu-ic-zip';
    if (['mp3', 'wav', 'ogg', 'm4a'].indexOf(e) > -1) return 'ppu-ic-audio';
    if (['mp4', 'webm', 'mov', 'mkv'].indexOf(e) > -1) return 'ppu-ic-video';
    return 'ppu-ic-file';
  }

  // ── one control ────────────────────────────────────────────────
  function enhance(root) {
    if (root._ppu) return; root._ppu = true;
    var cfg = {};
    try { cfg = JSON.parse(root.getAttribute('data-pp-uploader') || '{}'); } catch (e) {}
    cfg.connection = cfg.connection || 'local';
    cfg.chunkSize = cfg.chunkSize == null ? 4 * 1024 * 1024 : cfg.chunkSize;

    var hidden = root.querySelector('input.pp-up-hidden');
    if (!hidden) {
      hidden = h('input');
      hidden.type = 'hidden';
      hidden.className = 'pp-up-hidden';
      hidden.name = cfg.field || '';
      root.appendChild(hidden);
    }

    // Model: the ordered list of committed refs.
    var refs = [];
    try {
      var v = (hidden.value || '').trim();
      if (v) { var p = JSON.parse(v); refs = Array.isArray(p) ? p : [p]; }
    } catch (e) { refs = []; }
    // Server-resolved display URLs (parallel to refs) — keeps the hidden input
    // pristine while still showing thumbnails, incl. cloud signed URLs.
    if (Array.isArray(cfg.resolved)) {
      refs.forEach(function (r, i) {
        var d = cfg.resolved[i];
        if (d) { if (d.url) r.url = r.url || d.url; if (d.thumbUrl || d.url) r._preview = d.thumbUrl || d.url; }
      });
    }
    // A ref stripped of transient (underscore) keys — only this is persisted.
    function clean(r) { var o = {}; for (var k in r) { if (r.hasOwnProperty(k) && k.charAt(0) !== '_') o[k] = r[k]; } return o; }

    var drop = root.querySelector('.pp-up-drop') || (function () { var d = h('div', 'pp-up-drop'); root.insertBefore(d, hidden); return d; })();
    var list = root.querySelector('.pp-up-list') || (function () { var l = h('div', 'pp-up-list'); root.insertBefore(l, hidden.nextSibling); return l; })();
    var input = root.querySelector('input.pp-up-file') || (function () { var i = h('input'); i.type = 'file'; i.className = 'pp-up-file'; i.hidden = true; root.appendChild(i); return i; })();

    if (cfg.imageOnly) input.setAttribute('accept', 'image/*');
    else if (cfg.accept) input.setAttribute('accept', cfg.accept.split(',').map(function (e) { return '.' + e.trim(); }).join(','));
    if (cfg.multi) input.setAttribute('multiple', 'multiple');

    // Drop zone body.
    drop.innerHTML =
      '<div class="pp-up-drop-inner">' +
      '<span class="pp-up-drop-ic" aria-hidden="true"></span>' +
      '<span class="pp-up-drop-text"><b>Drag &amp; drop</b> ' + (cfg.imageOnly ? 'images' : 'files') + ' here, or <span class="pp-up-browse">browse</span></span>' +
      '<span class="pp-up-drop-hint">' + hintText(cfg) + '</span>' +
      '</div>';

    function commit() {
      var stored = refs.filter(function (r) { return !r._pending; }).map(clean);
      var val = '';
      if (stored.length) val = (cfg.multi ? JSON.stringify(stored) : JSON.stringify(stored[0]));
      hidden.value = val;
      hidden.dispatchEvent(new Event('change', { bubbles: true }));
      updateDropVisibility();
    }
    function updateDropVisibility() {
      var full = cfg.multi ? (cfg.maxFiles && refs.length >= cfg.maxFiles) : refs.length >= 1;
      drop.style.display = full ? 'none' : '';
    }

    // ── render existing / committed refs ──
    function refIsImage(ref) {
      return ref.thumb || (ref.mime || '').indexOf('image/') === 0 || isImgName(ref.name || ref.key);
    }
    // Thumbnail markup carries NO inline URL (a data-URI would break the HTML
    // attribute); card() sets the background via the style property instead.
    function refThumb(ref) {
      if (refIsImage(ref)) return '<span class="ppu-thumb"></span>';
      return '<span class="ppu-thumb ppu-ic ' + fileIconClass(ref.name || ref.key) + '"></span>';
    }
    // Safely apply a background image URL through the CSSOM (no HTML escaping traps).
    function setBg(el, url) {
      if (el && url) el.style.backgroundImage = "url('" + String(url).replace(/'/g, '%27') + "')";
    }
    function refUrl(ref, thumb) {
      if (thumb && ref._preview) return ref._preview;
      if (ref.url) return ref.url;
      if (ref._preview) return ref._preview;
      var key = (thumb && ref.thumb) ? ref.thumb : ref.key;
      if (key && (/^(https?:)?\/\//.test(key) || /^data:/.test(key))) return key;
      if (cfg.baseUploads && key) return cfg.baseUploads.replace(/\/$/, '') + '/' + String(key).replace(/^\//, '');
      return '';
    }

    function card(ref, idx) {
      var c = h('div', 'ppu-card');
      c.setAttribute('draggable', cfg.multi ? 'true' : 'false');
      c.innerHTML =
        refThumb(ref) +
        '<div class="ppu-meta"><div class="ppu-name" title="' + esc(ref.name || ref.key) + '">' + esc(ref.name || ref.key) + '</div>' +
        '<div class="ppu-sub">' + (ref.size ? bytes(ref.size) : '') + (ref.driver && ref.driver !== 'local' ? ' · ' + esc(ref.driver) : '') + '</div></div>' +
        '<div class="ppu-actions">' +
        (refUrl(ref) ? '<a class="ppu-open" href="' + esc(refUrl(ref)) + '" target="_blank" rel="noopener" title="Open">&#8599;</a>' : '') +
        '<button type="button" class="ppu-del" title="Remove">&times;</button></div>' +
        '<div class="ppu-progress" hidden><span></span></div>';
      if (refIsImage(ref)) setBg(c.querySelector('.ppu-thumb'), ref._preview || refUrl(ref, true));
      // Optional per-image alt text (accessibility) — kept on the ref, persisted.
      if (cfg.collectAlt && refIsImage(ref)) {
        var ai = h('input'); ai.type = 'text'; ai.className = 'ppu-alt-in';
        ai.placeholder = 'Describe this image (alt text)…'; ai.value = ref.alt || '';
        ai.addEventListener('input', function () { if (c._ref) c._ref.alt = ai.value; });
        ai.addEventListener('change', function () { if (c._ref) { c._ref.alt = ai.value; commit(); } });
        ai.addEventListener('click', function (e) { e.stopPropagation(); });
        c.querySelector('.ppu-meta').appendChild(ai);
      }
      c.querySelector('.ppu-del').addEventListener('click', function () {
        var i = refs.indexOf(ref); if (i > -1) refs.splice(i, 1);
        c.remove(); commit();
      });
      if (cfg.multi) wireReorder(c, ref);
      return c;
    }
    function wireReorder(c, ref) {
      c.addEventListener('dragstart', function (e) { c.classList.add('ppu-dragging'); try { e.dataTransfer.setData('text/plain', ''); } catch (x) {} });
      c.addEventListener('dragend', function () { c.classList.remove('ppu-dragging'); syncOrder(); });
      c.addEventListener('dragover', function (e) {
        e.preventDefault();
        var dragging = list.querySelector('.ppu-dragging'); if (!dragging || dragging === c) return;
        var r = c.getBoundingClientRect();
        list.insertBefore(dragging, (e.clientY - r.top) / r.height < 0.5 ? c : c.nextSibling);
      });
    }
    function syncOrder() {
      var order = [];
      list.querySelectorAll('.ppu-card').forEach(function (el) { if (el._ref) order.push(el._ref); });
      if (order.length === refs.length) { refs = order; commit(); }
    }

    function renderAll() {
      list.innerHTML = '';
      refs.forEach(function (ref, i) { var c = card(ref, i); c._ref = ref; list.appendChild(c); });
      updateDropVisibility();
    }

    // ── upload pipeline ──
    function addFiles(files) {
      var arr = Array.prototype.slice.call(files || []);
      if (!cfg.multi) arr = arr.slice(0, 1);
      arr.forEach(function (file) {
        var err = validate(file);
        if (err) { flash(err); return; }
        if (cfg.multi && cfg.maxFiles && refs.length >= cfg.maxFiles) { flash('Maximum ' + cfg.maxFiles + ' files.'); return; }
        if (!cfg.multi) { refs = []; list.innerHTML = ''; }
        uploadOne(file);
      });
    }
    function validate(file) {
      var e = extOf(file.name);
      var denied = ['php', 'phtml', 'phar', 'php3', 'php4', 'php5', 'pht', 'cgi', 'pl', 'sh', 'exe', 'htaccess'];
      if (denied.indexOf(e) > -1) return 'That file type is not allowed.';
      if (cfg.imageOnly && !isImgName(file.name) && (file.type || '').indexOf('image/') !== 0) return 'Only images are allowed.';
      if (cfg.accept) { var ok = cfg.accept.split(',').map(function (x) { return x.trim().toLowerCase(); }); if (ok.indexOf(e) < 0) return '.' + e + ' files are not allowed.'; }
      if (cfg.maxSize && file.size > cfg.maxSize) return file.name + ' is larger than ' + bytes(cfg.maxSize) + '.';
      return '';
    }

    function uploadOne(file) {
      // Optimistic card with progress.
      var ref = { name: file.name, size: file.size, mime: file.type, driver: cfg.connection === 'local' ? 'local' : '', _pending: true };
      refs.push(ref);
      var c = card(ref, refs.length - 1); c._ref = ref; list.appendChild(c);
      updateDropVisibility();
      var prog = c.querySelector('.ppu-progress'); prog.hidden = false;
      var bar = prog.firstChild;
      var thumb = c.querySelector('.ppu-thumb');

      function setProg(p) { bar.style.width = Math.round(p * 100) + '%'; }
      function done(resp) {
        var finalRef = (resp && resp.ref) ? resp.ref : resp;
        finalRef._preview = (resp && (resp.thumbUrl || resp.url)) || ref._preview;
        if (resp && resp.url && !finalRef.url) finalRef.url = resp.url;
        delete finalRef._pending;
        var i = refs.indexOf(ref);
        if (i > -1) { refs[i] = finalRef; }
        prog.hidden = true; c._ref = finalRef;
        if (finalRef._preview && thumb) { thumb.className = 'ppu-thumb'; setBg(thumb, finalRef._preview); }
        // refresh open link
        var open = c.querySelector('.ppu-open'); var u = refUrl(finalRef);
        if (u && !open) { var a = h('a', 'ppu-open'); a.href = u; a.target = '_blank'; a.innerHTML = '&#8599;'; c.querySelector('.ppu-actions').insertBefore(a, c.querySelector('.ppu-del')); }
        commit();
      }
      function fail(msg) {
        c.classList.add('ppu-error');
        prog.hidden = true;
        var i = refs.indexOf(ref); if (i > -1) refs.splice(i, 1);
        c.querySelector('.ppu-sub').textContent = msg || 'Upload failed';
        var retry = h('button', 'ppu-retry', 'Retry'); retry.type = 'button';
        retry.addEventListener('click', function () { c.remove(); uploadOne(file); });
        c.querySelector('.ppu-actions').insertBefore(retry, c.querySelector('.ppu-del'));
        commit();
      }

      // Local preview for images.
      if (isImgName(file.name) || (file.type || '').indexOf('image/') === 0) {
        try {
          var fr = new FileReader();
          fr.onload = function () { ref._preview = fr.result; if (thumb) { thumb.className = 'ppu-thumb'; setBg(thumb, fr.result); } };
          fr.readAsDataURL(file);
        } catch (e) {}
      }

      if (!cfg.endpoint) { setProg(1); prog.hidden = true; return; } // preview mode: no server

      maybeCompress(file, cfg).then(function (blob) {
        sendFile(blob, file.name, cfg, setProg, done, fail);
      });
    }

    // ── XHR send (chunked or single) ──
    function sendFile(blob, name, cfg, onProg, onDone, onFail) {
      var useChunks = cfg.chunkSize > 0 && blob.size > cfg.chunkSize;
      if (!useChunks) {
        var fd = new FormData();
        fd.append('file', blob, name);
        fd.append('field', cfg.field || '');
        fd.append('connection', cfg.connection || 'local');
        fd.append('folder', cfg.folder || '');
        if (cfg.private) fd.append('private', '1');
        if (cfg.token) fd.append('_token', cfg.token);
        var xhr = new XMLHttpRequest();
        xhr.open('POST', cfg.endpoint, true);
        xhr.upload.onprogress = function (e) { if (e.lengthComputable) onProg(e.loaded / e.total); };
        xhr.onload = function () { handleResp(xhr, onDone, onFail); };
        xhr.onerror = function () { onFail('Network error'); };
        xhr.send(fd);
        return;
      }
      // Chunked: append sequentially.
      var uploadId = 'u' + Date.now() + Math.random().toString(36).slice(2);
      var total = blob.size, offset = 0;
      function next() {
        var end = Math.min(offset + cfg.chunkSize, total);
        var slice = blob.slice(offset, end);
        var fd = new FormData();
        fd.append('file', slice, name);
        fd.append('field', cfg.field || '');
        fd.append('connection', cfg.connection || 'local');
        fd.append('folder', cfg.folder || '');
        if (cfg.private) fd.append('private', '1');
        fd.append('uploadId', uploadId);
        fd.append('offset', String(offset));
        fd.append('total', String(total));
        fd.append('name', name);
        if (cfg.token) fd.append('_token', cfg.token);
        var xhr = new XMLHttpRequest();
        xhr.open('POST', cfg.endpoint, true);
        xhr.onload = function () {
          if (xhr.status < 200 || xhr.status >= 300) { onFail('HTTP ' + xhr.status); return; }
          var r; try { r = JSON.parse(xhr.responseText); } catch (e) { onFail('Bad response'); return; }
          if (!r.success) { onFail(r.error || 'Upload failed'); return; }
          offset = end;
          onProg(offset / total);
          if (r.ref) { onDone(r); return; }   // final chunk returned the ref
          if (offset < total) next();
        };
        xhr.onerror = function () { onFail('Network error'); };
        xhr.send(fd);
      }
      next();
    }
    function handleResp(xhr, onDone, onFail) {
      if (xhr.status < 200 || xhr.status >= 300) { onFail('HTTP ' + xhr.status); return; }
      var r; try { r = JSON.parse(xhr.responseText); } catch (e) { onFail('Bad response'); return; }
      if (r && r.success && r.ref) onDone(r); else onFail((r && r.error) || 'Upload failed');
    }

    // ── flash message ──
    var flashEl;
    function flash(msg) {
      if (!flashEl) { flashEl = h('div', 'pp-up-flash'); root.appendChild(flashEl); }
      flashEl.textContent = msg; flashEl.classList.add('show');
      clearTimeout(flashEl._t); flashEl._t = setTimeout(function () { flashEl.classList.remove('show'); }, 3500);
    }

    // ── events ──
    drop.addEventListener('click', function () { input.click(); });
    drop.querySelector('.pp-up-browse') && drop.querySelector('.pp-up-browse').addEventListener('click', function (e) { e.stopPropagation(); input.click(); });
    input.addEventListener('change', function () { addFiles(input.files); input.value = ''; });
    ['dragenter', 'dragover'].forEach(function (ev) { drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add('pp-up-over'); }); });
    ['dragleave', 'drop'].forEach(function (ev) { drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove('pp-up-over'); }); });
    drop.addEventListener('drop', function (e) { if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files); });
    // paste images
    root.addEventListener('paste', function (e) {
      if (!e.clipboardData) return;
      var imgs = [];
      Array.prototype.forEach.call(e.clipboardData.files || [], function (f) { imgs.push(f); });
      if (imgs.length) addFiles(imgs);
    });

    renderAll();
  }

  function hintText(cfg) {
    var bits = [];
    if (cfg.accept) bits.push(cfg.accept.split(',').map(function (x) { return x.trim().toUpperCase(); }).join(' · '));
    else if (cfg.imageOnly) bits.push('Images');
    if (cfg.maxSize) bits.push('up to ' + bytes(cfg.maxSize));
    if (cfg.multi && cfg.maxFiles) bits.push('max ' + cfg.maxFiles + ' files');
    return bits.join(' • ');
  }

  function parseAspect(s) {
    if (!s) return 0;
    var m = /^(\d+(?:\.\d+)?)\s*[:/x]\s*(\d+(?:\.\d+)?)$/.exec(String(s).trim());
    if (m) return parseFloat(m[1]) / parseFloat(m[2]);
    var n = parseFloat(s); return n > 0 ? n : 0;
  }
  // Optional client-side crop-to-aspect and/or downscale before upload (best-effort).
  function maybeCompress(file, cfg) {
    var c = cfg.compress || {};
    var ratio = parseAspect(cfg.cropAspect);
    var wantScale = !!c.maxDim, wantCrop = ratio > 0;
    if ((!wantScale && !wantCrop) || (file.type || '').indexOf('image/') !== 0 || /svg|gif/i.test(file.type)) return Promise.resolve(file);
    if (typeof createImageBitmap !== 'function' || !document.createElement('canvas').toBlob) return Promise.resolve(file);
    return createImageBitmap(file, { imageOrientation: 'from-image' }).then(function (bmp) {
      var sw = bmp.width, sh = bmp.height, sx = 0, sy = 0;
      if (wantCrop) {
        var cur = sw / sh;
        if (cur > ratio) { var nw = Math.round(sh * ratio); sx = Math.round((sw - nw) / 2); sw = nw; }
        else if (cur < ratio) { var nh = Math.round(sw / ratio); sy = Math.round((sh - nh) / 2); sh = nh; }
      }
      var scale = wantScale ? Math.min(1, c.maxDim / Math.max(sw, sh)) : 1;
      if (!wantCrop && scale >= 1) { bmp.close && bmp.close(); return file; } // nothing to do
      var dw = Math.max(1, Math.round(sw * scale)), dh = Math.max(1, Math.round(sh * scale));
      var cv = document.createElement('canvas'); cv.width = dw; cv.height = dh;
      cv.getContext('2d').drawImage(bmp, sx, sy, sw, sh, 0, 0, dw, dh);
      bmp.close && bmp.close();
      return new Promise(function (res) {
        cv.toBlob(function (b) { res(b && (wantCrop || b.size < file.size) ? b : file); }, 'image/jpeg', c.quality || 0.85);
      });
    }).catch(function () { return file; });
  }

  window.__ppUploaderInit = function (root) {
    (root || document).querySelectorAll('.pp-uploader').forEach(enhance);
  };
  if (document.readyState !== 'loading') window.__ppUploaderInit();
  else document.addEventListener('DOMContentLoaded', function () { window.__ppUploaderInit(); });
})();
