/*
 * Hanis instance handler object.
 * Typically called from module edit.
 *
 * Uses Object Literal format.
 *
 * @package    mod_wfsim
 * @copyright 2015 onwards Catalyst IT Europe Ltd
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 *
 */

// Object in global namespace.

var Hinst = {
  inst: 0,
  cmid: 0,
  acceptedTypes: {// Object as associative array.
    'image/png': true,
    'image/jpeg': true,
    'image/gif': true
  },
  // These are global (ie throughout all the page) variables.
  imageBase: '',
  imageWidth: 100,
  imageHeight: 80,
  imageWidthBulk: 25,
  imageHeightBulk: 20,
  casedd: 0,
  curRow: 0,
  curCol: 1,
  addimgmsg: '<span class="addimage">add image</span>',
  debug: false,
  // Setup.
  setup: function(cmid, inst, sesskey, debug) {
    Hinst.cmid = cmid;
    Hinst.inst = inst;
    Hinst.sesskey = sesskey;
    Hinst.debug = debug;
    Hinst.imageBase = $('#id_' + Hinst.inst + '_hinst_image_base').val().trim(); // Eg id_1_hinst_image_base ('nwp00/300hpa').
    Hinst.overlayBase = $('#id_' + Hinst.inst + '_hinst_overlay_base').val().trim();
    Hinst.title = $('#id_' + Hinst.inst + '_hinst_title').val().trim();

    console.log('Hinst v1.1: cmid=' + Hinst.cmid + ' inst=' + Hinst.inst + ' imageBase=' + Hinst.imageBase + ' debug=' + Hinst.debug);
    Hinst.debug && console.log('sesskey='+Hinst.sesskey+' jquery-ui ver='+$.ui.version);
    if (Hinst.imageBase.length == 0) {
      console.log('Missing imageBase=' + Hinst.imageBase);
      alert('Missing Image base');
      return false;
    }

    // Store Case dd as number - should always be set as its set to today by default in mod_form.
    Hinst.casedd = $('#id_simpage_dtm_day').val() * 1;
    Hinst.debug && console.log('casedd=' + Hinst.casedd);

    // Clear any existing (on any inst...).
    Hinst.curRow = 0;
    Hinst.curCol = 1;
    $('#ddgroup').remove();

    // Create widget under body.
    $('body').append(
            '<div id="ddgroup"> \
            <div class="title">Hinst' + Hinst.inst + ' ' + Hinst.title + ' (' + Hinst.imageBase + ')</div> \
            <div class="hrow" id="r0"> \
              <div class="hdr" id="c0">Main</div> \
            </div> \
          </div>');

    // Enable widget to be moved on page.
    //$('#ddgroup').draggable({
    //  handle: '.title',
    //  cursor: 'move'
    //}); // (causes window to disappear when move tried - if ddgroup is position:absolute).
    //$('#ddgroup').css('position', 'absolute');
    //$('#ddgroup').css('top', '600px');
    // Position at top of viewport (on mod_form, the 'previous positioned element' is BODY).
    $('#ddgroup').css('top', ($(window).scrollTop() + 45));
    $('#ddgroup').css('left', '400px');

    // Stop images dropped outside dropzone resulting in the image replacing the page.
    window.addEventListener("dragover", function(e) {
      e = e || event;
      e.preventDefault();
    }, false);
    window.addEventListener("drop", function(e) {
      e = e || event;
      e.preventDefault();
    }, false);

    // Get styles from module stylesheet for dragenter/leave, drop events. Function is in Utils.
    Hinst.dragenterstyle = getCssRules('#ddgroup .dropzone #dragenterstyle');
    if ($.isEmptyObject(Hinst.dragenterstyle)) {
      Hinst.dragenterstyle = {'box-shadow': '0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(82, 168, 236, 0.6)'};
    }
    Hinst.dragleavestyle = getCssRules('#ddgroup .dropzone #dragleavestyle');
    if ($.isEmptyObject(Hinst.dragleavestyle)) {
      Hinst.dragleavestyle = {'box-shadow': 'none'};
    }
    Hinst.dropzonestyle = getCssRules('#ddgroup .dropzone #dropzonestyle');
    if ($.isEmptyObject(Hinst.dropzonestyle)) {
      Hinst.dropzonestyle = {'box-shadow': 'none'};
    }

    // Load existing.
    var fn_sel = '#id_' + Hinst.inst + '_hinst_filenames';
    var fl_sel = '#id_' + Hinst.inst + '_hinst_frame_labels';
    var of_sel = '#id_' + Hinst.inst + '_hinst_overlay_filenames';
    var ol_sel = '#id_' + Hinst.inst + '_hinst_overlay_labels';

    var curFilenames = $(fn_sel).val();
    if (curFilenames.length > 0) {
      // (.split will return 1 instance if string empty).
      var curFilenamesa = curFilenames.split(',');
      var curFrameLabelsa = $(fl_sel).val().split(',');
      Hinst.debug && console.log('curFilenamea len=' + curFilenamesa.length);
      Hinst.debug && console.log('curFrameLabelsa len=' + curFrameLabelsa.length);

      for (var r = 1; r <= curFilenamesa.length; r++) {
        var fn = curFilenamesa[r - 1].trim();
        var fl = curFrameLabelsa[r - 1].trim();
        $('#ddgroup').append(this.htmlRow(r, fn, fl));

        this.loadThumbnail('r' + r + 'c0', fn, false);
        Hinst.curRow++;
      }
    }

    // Add Overlays.
    var curOvLabels = $(ol_sel).val(); // 300hPa Contours, 300hPa Wind.
    if (curOvLabels.length > 0) {
      // Split out overlay groups.
      var curOvFilenamesa = $(of_sel).val().split(',');
      var curOvLabelsa = curOvLabels.split(',');
      // co140000.gif & co140100.gif, wd140000.png & wd140100.png.
      for (var c = 1; c <= curOvLabelsa.length; c++) {
        this.addOvCol(false);
        var ol = curOvLabelsa[c - 1].trim();
        $('#ddgroup #c' + c + ' input').val(ol);
        $('#ddgroup #c' + c + ' input').attr('title', ol); // Add tooltip.
        // Get files in this overlay.
        var ofna = curOvFilenamesa[c - 1].split('&');
        for (var r = 1; r <= ofna.length; r++) {
          var of = ofna[r - 1].trim();
          this.loadThumbnail('r' + r + 'c' + c, of, true);
        }
      }
    }

    $('#ddgroup').append('<div id="addrow" class="group">Add Row</div>');
    $('#ddgroup').append('<div id="addcol" class="group">Add Overlay</div>');
    $('#ddgroup').append('<div id="addcolbulk" class="group">Add Bulk</div>');
    $('#ddgroup').append('<div id="saveselection" class="group">Save</div>');
    $('#ddgroup').append('<div id="exit" class="group"></div>');

    // Add empty row.
    this.addRow(false);

    // Add handlers.
    this.handlers();
    this.dndHandler();

  },

  // Functions.
  htmlRow: function(r, filename, frameLabel) {
    return ' \
      <div id="r' + r + '" class="row"> \
        <div id ="r' + r + 'c0" class="col col0"> \
          <div class="dropzone"></div> \
          <div class="fnameholder">' + filename + '</div> \
          <div class="flabelname"><input class="labelname" type="text" value="' + frameLabel + '"/></div> \
          <div class="fuploadstatus"></div> \
          <div class="deleterow"></div> \
        </div> \
      </div>';
  },

  addRow: function(doDnD) {
    Hinst.curRow++;
    $('#ddgroup #addrow').before(Hinst.htmlRow(Hinst.curRow, Hinst.addimgmsg, ''));
    Hinst.debug && console.log('addRow: new=r' + Hinst.curRow + ' curCol=' + Hinst.curCol);

    // Add overlay col cells.
    var curcols = $("#ddgroup #r0 .hdr").length - 1; // Get num of actual cols.
    for (var c = 1; c <= curcols; c++) {
      $("#ddgroup .row#r" + Hinst.curRow).append(Hinst.htmlOvCol(Hinst.curRow, c));
    }

    // And extend width based on general row width. TODO remove need for width setting on rows?
    $("#ddgroup .row#r" + Hinst.curRow).width($("#ddgroup .row").width());

    if (doDnD) {
      Hinst.dndHandler();
    }

    $('#ddgroup #r' + Hinst.curRow + ' .deleterow').click(function(e) {
      Hinst.deleteRow(e);
    });

  },

  deleteRow: function(e) {
    e.stopPropagation();
    e.preventDefault();
    var rowid = $(e.target).parent().parent().attr('id'); // Eg r23 .

    if (typeof rowid == 'undefined' || rowid.charAt(0) != 'r') {
      debugEvent(e, false);
      Hinst.debug && console.log('deleteRow: missing/invalid rowid id [' + rowid + ']');
      return;
    }

    // Gets called once for each row so quit when we've obviously already done delete.
    var delcols = $('#ddgroup #' + rowid + ' .col').length;
    Hinst.debug && console.log('deleteRow: delcols len=' + delcols);
    if (delcols == 0) {
      return;
    }

    // Remove all col items on row.
    $('#ddgroup #' + rowid + ' .col').each(function(idx) {
      var fn = $(this).find('.fnameholder').html();
      var ov = (idx == 0 ? false : true);
      Hinst.debug && console.log('deleteRow() rowid=' + rowid + ' fn=' + fn + ' ov=' + ov);
      if (fn != Hinst.addimgmsg && typeof fn != 'undefined') {
        Hinst.deleteFile(fn, ov); // Delete on remote.
      } // Else img not loaded.
    });

    // Could just remove element - but using id makes it more certain.
    $('#ddgroup #' + rowid).remove();
  },

  // Add items on to right of base items col.
  htmlOvCol: function(r, c) {
    return ' \
      <div class="col col' + c + '" id="r' + r + 'c' + c + '"> \
        <div class="dropzone"></div> \
        <div class="fnameholder"><span class="addimage">add image</span></div> \
        <div class="fuploadstatus"></div> \
      </div>';
  },

  htmlOvColHdr: function(c) {
    return ' \
      <div class="hdr col' + c + '" id="c' + c + '"> \
        <input type="text" value="" class="labelname"/> \
        <div class="deletecol"></div> \
      </div>';
  },

  htmlColBulk: function() {
    return ' \
      <div class="colbulk" id="bulk"> \
        <div id="bulkhdr" class="group">Drop images below</div> \
        <div id="exitbulk" class="group"></div> \
        <div class="dropzone"></div> \
        <div class="fnameholder"></div> \
        <div class="fuploadstatus"></div> \
        <div id="progress"></div> \
        <div id="savebulk" class="group">Save</div> \
      </div>';
  },

  addOvCol: function(doDnD) {
    // Widen row.
    var curWidth = $("#ddgroup .row").width();
    $("#ddgroup .row").width(curWidth + 118); // TODO put size in css?

    Hinst.debug && console.log('addcol: curCol=' + Hinst.curCol + ' curWidth=' + curWidth);

    $("#ddgroup .hrow").append(Hinst.htmlOvColHdr(Hinst.curCol));

    $("#ddgroup .row").each(function() {
      var r = $(this).attr('id').substr(1);
      $(this).append(Hinst.htmlOvCol(r, Hinst.curCol));
    });

    if (doDnD) {
      Hinst.dndHandler();
    }

    // Register handlers.
    $(".deletecol").click(function(e) {
      Hinst.deleteOvCol(e);
    });

    Hinst.curCol++;
  },

  deleteOvCol: function(e) {
    var colid = $(e.target).parent().attr('id'); // c2
    e.stopPropagation();
    e.preventDefault();

    Hinst.debug && console.log('deleteOvCol: parent colid [' + colid + ']');

    if (typeof colid == 'undefined' || colid.charAt(0) != 'c') {
      debugEvent(e, false);
      Hinst.debug && console.log('deleteOvCol: missing/invalid parent id [' + colid + ']');
      return;
    }
    var delcol = colid.substr(1);

    // Gets called once for each col so quit when we've obviously already done delete.
    var delcols = $("#ddgroup .col" + delcol).length;
    Hinst.debug && console.log('deleteOvCol: delcols len=' + delcols);
    if (delcols == 0) {
      return;
    }

    // Remove all items with '.col'+c on each row.
    $("#ddgroup .col" + delcol).each(function(idx) {
      if (idx == 0) { // Ov label.
        return true; // continue
      }
      // Remove image if present.
      var fn = $(this).find('.fnameholder').html();
      Hinst.debug && console.log('deleteOvCol() colid=' + colid + ' fn=' + fn);
      if (fn != Hinst.addimgmsg && typeof fn != 'undefined' && fn != '') {
        Hinst.deleteFile(fn, true); // Delete on remote.
      } // Else img not loaded.
    });

    // Could just remove element - but using class makes it more certain.
    $("#ddgroup .col" + delcol).remove();

    // Shrink width.
    var curWidth = $("#ddgroup .row").width();
    $("#ddgroup .row").width(curWidth - 118);
  },

  // To add a bulk set of images to a column, display the drop panel and when images dropped, display in panel.
  // On save, upload images to server and pull thumbnails back to display in next free column.
  addColBulk: function() {
    var filesArray = [];
    // Clear any existing.
    $('#ddgroup #bulk').remove();

    // Create new.
    $("#ddgroup").append(Hinst.htmlColBulk());

    $('#ddgroup #bulk').css('top', '20px');
    // Scroll so bulk dialogue is in view.
    var bulkoffset = $('#ddgroup #bulk').offset();
    window.scrollTo(0, bulkoffset.top - 50);

    // Enable widget to be moved on page.
    // Note: if div needs a scroll bar, the bar cannot be dragged with mouse - OK.
    $('#ddgroup #bulk').draggable({
      handle: '#bulkhdr',
      cursor: 'move'
      // If use handle, you can still drag by another area but starts moving rows out of the way... (?)
    });

    // Handlers.
    // Switch off single dnd, and switch on bulk.
    Hinst.dndHandler('#ddgroup .row .col .dropzone', 'off');
    Hinst.dndHandler('#ddgroup #bulk .dropzone', 'on');

    // Set up our own drop handler.
    $('#ddgroup #bulk .dropzone').off('drop'); // Clear any existing.
    $('#ddgroup #bulk .dropzone').on('drop', function(e) {
      Hinst.debug && console.log('addColBulk: drop');
      e.stopPropagation();
      e.preventDefault();

      // Show in dropzone.
      fa = Hinst.processDropBulk(e);
      // Merge latest drop into filesArray.
      // Note that filesArray is treated as a static when handler is in main function where
      // vbl is defined. If moved to own function the vbl would have to be global.
      $.merge(filesArray, fa);
      Hinst.debug && console.log('addColBulk: drop, filesArray=' + filesArray);
    });

    // Handle save.
    $("#ddgroup #savebulk").click(function(e) {
      Hinst.saveBulk(filesArray);

      Hinst.dndHandler('#ddgroup #bulk .dropzone', 'off');
      Hinst.dndHandler('#ddgroup .row .col .dropzone', 'both');
      $(this).parent().fadeOut();
      $(this).parent().css('display', 'none');

      // Scroll to Widget Save.
      var butoffset = $('#ddgroup #saveselection').offset();
      window.scrollTo(0, butoffset.top);
    });

    // Handle exit.
    $("#ddgroup #exitbulk").click(function(e) {
      Hinst.dndHandler('#ddgroup #bulk .dropzone', 'off');
      Hinst.dndHandler('#ddgroup .row .col .dropzone', 'both');
      $(this).parent().fadeOut();
      $(this).parent().css('display', 'none');
    });

  },

  saveBulk: function(filesArray) {
    var bulkcol = -1;

    if (filesArray == null || filesArray.length == 0) {
      // No files dropped - quit.
      $('#ddgroup #bulk').fadeOut();
      return;
    }

    // Send & push into a column.

    // Find first empty col (create if none).
    var neednew = true;
    // Note: col numbers may not be contiguous if manually added/removed earlier.
    $("#ddgroup #r0 .hdr").each(function() {
      var c = $(this).attr('id').substr(1); // c0, c1
      var imgs = $("#ddgroup .col" + c + " .dropzone img");
      if (imgs.length == 0) {
        // Found empty col.
        neednew = false;
        bulkcol = c;
        return false; // break.
      }
    });

    if (neednew) {
      Hinst.addOvCol(true);
      bulkcol = Hinst.curCol - 1;
    }

    // Check rows.
    var numbulk = filesArray.length;
    var numrows = $("#ddgroup .col" + bulkcol + " .dropzone").length;
    while (numbulk > numrows) {
      // Need more rows.
      Hinst.addRow(true);
      numrows++;
    }

    // Sort into filename order.
    filesArray.sort(function(a, b) {
      if (a.name > b.name) {
        return 1;
      }
      if (a.name < b.name) {
        return -1;
      }
      return 0;
    });

    // Send and move over to main widget.
    var ov = (bulkcol > 0 ? true : false);
    for (var i = 0; i < filesArray.length; i++) {
      var file = filesArray[i];
      Hinst.debug && console.log('saveBulk: file ' + i + '=' + file.name);
      Hinst.sendFile(file, 'bulk', ov);
      // Set thumbnail on main widget.
      Hinst.loadThumbnail('r' + (i + 1) + 'c' + bulkcol, file.name, ov);
      // Add label if main col.
      if (bulkcol == 0) {
        $('#ddgroup #r' + (i + 1) + 'c0 input.labelname').val(Hinst.getLabelFromFilename(file.name));
      }
    }

    // Show total bulk dropped.
    $("#ddgroup").append('<div id="bulknumdropped">Number uploaded=' + numrows + '</div>');
    $("#ddgroup #bulknumdropped").fadeOut(5000);
  },

  loadThumbnail: function(rcid, filename, ov) {
    var image = new Image();
    var baseurl = '/mod/wfsim/wffile.php?p=images/' + Hinst.imageBase;

    // Use overlayBase if set else add overlay subdir to imageBase.
    if (ov) {
      if (Hinst.overlayBase.length != 0) {
        baseurl = '/mod/wfsim/wffile.php?p=images/' + Hinst.overlayBase;
      } else {
        baseurl += '/ov';
      }
    }
    baseurl += '/';

    // Allow time for thumbnail to be generated on server.
    //image.src = '/mod/wfsim/pix/default_thumb.png'; // Show default if not found in time?
    Hinst.checkUrlExist(500, baseurl + 'thumb_' + filename, 6); // millisec, fn, tries.
    image.src = baseurl + 'thumb_' + filename;  // Load thumbnail file into image.
    image.width = Hinst.imageWidth; // Force width.
    image.height = Hinst.imageHeight; // Force height.

    // If 'add image' present - remove and fill in filename.
    if ($('#' + rcid + ' .fnameholder .addimage').length > 0) {
      $('#' + rcid + ' .fnameholder .addimage').remove();
      $('#' + rcid + ' .fnameholder').html(filename);
    }

    $('#' + rcid + ' .dropzone').append(image);
  },

  // Try c times to get url, pausing msec millisecs.
  // Although not loading into image.src directly - browser seems to put in image when sucessful, so avoiding broken image.
  checkUrlExist: function(msec, url, c) {
    $.get(url)
            .done(function() {
              //console.log('checkUrlExist: OK: '+url+' c='+c);
              return true;
            }).fail(function() {
      if (c == 0) {
        Hinst.debug && console.log('checkUrlExist: Give up! [' + url + '] c=' + c);
        return false;
      }
      //console.log('checkUrlExist: Try again... c='+c+' '+url);
      // Call ourself with timeout and passing timeout-val etc to ourself.
      setTimeout(Hinst.checkUrlExist, msec, msec, url, c - 1);
    });
  },

  sendFile: function(file, parid, ov) {
    var baseurl = '/mod/wfsim/uploadfile.php?cmid=' + Hinst.cmid + '&sesskey=' + Hinst.sesskey + '&imagebase=' + Hinst.imageBase;

    // Use overlayBase if set else add overlay subdir to imageBase.
    if (ov) {
      if (Hinst.overlayBase.length != 0) {
        baseurl = '/mod/wfsim/uploadfile.php?cmid=' + Hinst.cmid + '&sesskey=' + Hinst.sesskey + '&imagebase=' + Hinst.overlayBase;
      } else {
        baseurl += '/ov';
      }
    }
    baseurl += '/';

    // File properties are readonly, so pass extra params in GET.
    var formData = new FormData();
    formData.append('myfile', file);

    // Ref http://api.jquery.com/jquery.ajax/
    var jqXHR = $.ajax({
      xhr: function() {
        var xhrobj = $.ajaxSettings.xhr();
        return xhrobj;
      },
      url: baseurl,
      type: "POST",
      contentType: false,
      processData: false,
      cache: false,
      data: formData,
      success: function(data, textStatus) {
        // Remote should return any error msgs in format: 'error: xxx'.
        Hinst.debug && console.log('sendFile: ajax resp success: file=' + file.name + ' data=' + data + ' textStatus=' + textStatus);
        if (data.indexOf('error:') != -1) {
          data = data.replace(/error:/g, ' ');
          Hinst.showMsgBox("#" + parid, data);
          $("#" + parid + " .fuploadstatus").html('Error').addClass('error').removeClass('ok');
        } else {
          $("#" + parid + " .fuploadstatus").html('OK').addClass('ok').removeClass('error');
        }
      }
    });
  },

  deleteFile: function(filename, ov) {
    var baseurl = '/mod/wfsim/uploadfile.php?deletefile=' + filename + '&cmid=' + Hinst.cmid + '&sesskey=' + Hinst.sesskey +
                   '&imagebase=' + Hinst.imageBase;

    // Use overlayBase if set else add overlay subdir to imageBase.
    if (ov) {
      if (Hinst.overlayBase.length != 0) {
        baseurl = '/mod/wfsim/uploadfile.php?deletefile=' + filename + '&cmid=' + Hinst.cmid + '&sesskey=' + Hinst.sesskey +
                    '&imagebase=' + Hinst.overlayBase;
      } else {
        baseurl += '/ov';
      }
    }

    var jqXHR = $.ajax({
      xhr: function() {
        var xhrobj = $.ajaxSettings.xhr();
        return xhrobj;
      },
      url: baseurl,
      type: "GET",
      contentType: false,
      processData: false,
      cache: false,
      data: '',
      success: function(data, textStatus) {
        Hinst.debug && console.log('deleteFile: ' + Hinst.imageBase + ' ' + filename + ' res=' + data + ' text=' + textStatus);

        if (data.indexOf('error:') != -1) {
          data = data.replace(/error:/g, ' ');
          Hinst.showMsgBox('', data);
        } else {
          Hinst.debug && console.log('deleteFile: ' + filename + ' OK');
        }
      }
    });
  },

  processDrop: function(e) {
    // If img/span etc present in DIV.dropzone, e.currentTarget shows DIV.dropzone, e.target is IMG.
    // If DIV.dropzone empty both are DIV.dropzone.
    var rowid = $(e.currentTarget).parent().parent().attr('id'); // r23.
    var rcid = $(e.currentTarget).parent().attr('id'); // r2c4.
    var rc = Hinst.getRC(rcid);
    var ov = false; // Overlay flag.
    if (rc.c > 0) {
      ov = true;
    }

    var imagecount = $("#" + rcid + " .dropzone img").length;

    Hinst.debug && console.log('processDrop() imagecount=' + imagecount + ' rowid=' + rowid + ' rcid=' + rcid + ' rc=' + rc);

    if (typeof rcid == 'undefined' || rcid.charAt(0) != 'r') {
      Hinst.debug && console.log('processDrop: missing/invalid rcid [' + rcid + ']');
      return;
    }

    // Replace existing rather than prevent drop.
    if (imagecount > 0) {
      Hinst.debug && console.log('processDrop: image exists [' + rcid + '], removing');
      // Need to remove img object, label, filename.
      var fn = $('#' + rcid + ' .fnameholder').text();
      Hinst.deleteFile(fn, ov);
      $('#' + rcid + ' .dropzone img').remove();
      $('#' + rcid + ' .fnameholder').text('');
      $('#' + rcid + ' .labelname').text('');
    }

    $("#" + rcid + " .dropzone").css(Hinst.dropzonestyle);

    var filesArray = e.originalEvent.dataTransfer.files;
    // Only accept first one if multiple dragged.
    var file = filesArray[0];

    if (this.acceptedTypes[file.type] === true) {

      // Check filename has same day as case day or day after (for sims over midnight).
      var filedd = file.name.substr(2, 2);
      //Hinst.debug && console.log('processDrop: casedd='+casedd+' file dd='+filedd);
      if (isNaN(filedd * 1)) {
        Hinst.showMsgBox("#" + rcid, 'Please ensure dd characters in file name are linked to case dd (' + Hinst.casedd + ')');
        return; // File not saved on server.
      }
      if (filedd != Hinst.casedd && filedd != Hinst.casedd + 1) {
        Hinst.showMsgBox("#" + rcid,
                'Please ensure dd characters in file name (currently ' + filedd + ') are linked to case dd (' + Hinst.casedd + ')');
        return; // File not saved on server.
      }

      // Display file name.
      $("#" + rcid + " .fnameholder").html(file.name);

      // Set default label, based on filename.
      $("#" + rcid + " .labelname").val(Hinst.getLabelFromFilename(file.name));

      // Do upload.
      this.sendFile(file, rcid, ov);

      // Show on screen.
      this.previewFile(file, rcid);
    } else {
      Hinst.debug && console.log('processDrop: invalid file type=' + file.type);
      $("#" + rcid + " .fuploadstatus").html("invalid type").css('color', 'red');
    }

    // Auto add new row.
    if (rc.c == 0) {
      var ero = Hinst.checkEmptyRow();
      if (ero.emptyrowa.length == 0 || (ero.emptyrowa.length == 1 && ero.emptyrowa[0] == rc.r)) {
        Hinst.addRow(true);
      }
    }
  },

    processDropBulk: function(e) {
    var imagecount = $("#ddgroup #bulk .dropzone img").length;
    Hinst.debug && console.log('processDropBulk: cur imagecount=' + imagecount);

    // Indeterminate progress indicator (https://api.jqueryui.com/progressbar).
    $("#ddgroup #bulk #progress").progressbar({
      value: false
    });

    $("#ddgroup #bulk .dropzone").css(Hinst.dropzonestyle);

    // Process returned array - does not seem to be full array object.
    var filesArray = e.originalEvent.dataTransfer.files;
    for (var i = 0; i < filesArray.length; i++) {
      var file = filesArray[i];

      // Check filename has same day as case day or day after (for sims over midnight).
      var filedd = file.name.substr(2, 2);
      if (isNaN(filedd * 1)) {
        Hinst.showMsgBox("#bulk", 'Please ensure dd characters in file name are linked to case dd (' + Hinst.casedd + ')');
        filesArray = [];
        break;
      }
      if (filedd != Hinst.casedd && filedd != Hinst.casedd + 1) {
        Hinst.showMsgBox("#bulk",
                'Please ensure dd characters in file name (currently ' + filedd + ') are linked to case dd (' + Hinst.casedd + ')');
        filesArray = [];
        break;
      }

      if (this.acceptedTypes[file.type] === true) {
        // Show on screen.
        Hinst.previewFile(file, 'bulk');
      } else {
        Hinst.debug && console.log('processDropBulk: invalid file type=' + file.type + ' fn=' + file.name);
        $("#ddgroup #bulk .fnameholder").text('invalid file [' + file.name + ']').css('color', 'red');
      }
    }

    $("#ddgroup #bulk #progress").progressbar('destroy');

    return filesArray;
  },

  previewFile: function(file, parid) {
    // ref: https://developer.mozilla.org/en/docs/Web/API/FileReader
    var reader = new FileReader();

    // When read event completed.
    reader.onload = function(event) {
      var image = new Image();
      image.src = event.target.result;
      // Force width/height.
      if (parid == 'bulk') {
        image.width = Hinst.imageWidthBulk;
        image.height = Hinst.imageHeightBulk;
      } else {
        image.width = Hinst.imageWidth;
        image.height = Hinst.imageHeight;
      }
      $("#" + parid + " .dropzone").append(image);
    };

    // Do the read.
    reader.readAsDataURL(file);
    //holder.innerHTML += '<p>Uploaded ' + file.name + ' ' + (file.size ? (file.size/1024|0) + 'K' : '');
    Hinst.debug && console.log('previewFile: parid=' + parid + ' file=' + file);
    return;
  },


  // At this point the files are setup in widget and uploaded to correct place on server
  // (defined in #id_1_hinst_image_base).
  // The widget table is vetted for empty cells/fields.
  // On save we update the (hidden) edit mod_form fields, #id_1_hinst_filenames,
  // #id_1_hinst_frame_labels etc.
  saveSelection: function() {
    var error = false;
    var filenames = [];
    var frame_labels = [];
    var overlay_filenames = [];
    var overlay_labels = [];
    var numov = $('#ddgroup .hrow input').length;

    Hinst.debug && console.log('saveSelection: numov=' + numov);

    // Remove empty rows;
    var ero = Hinst.checkEmptyRow();
    for (var i = 0; i < ero.emptyrowa.length; i++) {
      Hinst.debug && console.log('removing ' + '#r' + ero.emptyrowa[i] + '.row');
      $('#r' + ero.emptyrowa[i] + '.row').remove();
    }

    // For each row get main col, get fn,fl, then each overlay col's ovfn. Get ov labels later.
    $('#ddgroup .row .col0').each(function(r) {
      var emptyrow = true;
      var fn = $(this).find('.fnameholder').html();
      var fl = $(this).find('.flabelname input').val().trim();

      $(this).removeClass('rowerror'); // Clear down previous group error settings.

      if ($(this).find('.fuploadstatus').hasClass('error')) {
        error = true;
        emptyrow = false; // Assume img there.
        return true; // Continue.
      }

      if (fl.length == 0) {
        $(this).addClass('rowerror');
        error = true;
      } else {
        filenames.push(fn);
        frame_labels.push(fl);
        emptyrow = false;
      }

      // Do overlays of this row.
      var rovfn = [];
      overlay_filenames.push(rovfn);
      $(this).nextAll().each(function(c) { // Sibling cols.
        if ($(this).children('.fuploadstatus').hasClass('error')) {
          error = true;
          emptyrow = false; // Assume img there.
          return true; // continue
        }
        var ovfn = $(this).children('.fnameholder').html();
        if (ovfn == Hinst.addimgmsg || ovfn.length == 0) {
          $(this).children('.dropzone').addClass('colerror');
          // Need to remove element style, set in dnd handler.
          $(this).children('.dropzone').attr('style', '');
          error = true;
        } else {
          overlay_filenames[r].push(ovfn);
          emptyrow = false; // Assume img there.
        }
      });

      if (emptyrow) {
        $(this).addClass('rowerror');
      }

    });

    // Get overlay labels.
    $('#ddgroup .hrow input').each(function(c) {
      var ovl = $(this).val();
      if (ovl.length == 0) {
        $(this).parent().addClass('colerror');
        error = true;
      } else {
        $(this).parent().removeClass('colerror');
        overlay_labels.push(ovl.trim());
      }
    });

    Hinst.debug && console.log('fnames=' + filenames.join(', '));
    Hinst.debug && console.log('labels=' + frame_labels.join(', '));

    // Transpose r-c arrays into c-r arrays.
    // Add empty col array object for row-entries.
    var covfn = [];
    for (var c = 0; c < numov; c++) {
      var aa = [];
      covfn.push(aa);
    }
    for (var r = 0; r < overlay_filenames.length; r++) {
      for (var c = 0; c < numov; c++) {
        covfn[c].push(overlay_filenames[r][c]);
      }
    }
    // Now build array of joined ovfn cols, ready to joined into form val.
    var ovfns = [];
    for (var c = 0; c < covfn.length; c++) {
      ovfns.push(covfn[c].join(' & '));
    }
    Hinst.debug && console.log('ovfns=' + ovfns.join(', '));
    Hinst.debug && console.log('ovlabels=' + overlay_labels.join(', '));

    if (! error) {
      // Set form fields to new values as CSV.
      $('#id_' + Hinst.inst + '_hinst_filenames').val(filenames.join(', '));
      $("#id_" + Hinst.inst + "_hinst_frame_labels").val(frame_labels.join(', '));
      $("#id_" + Hinst.inst + "_hinst_overlay_filenames").val(ovfns.join(', '));
      $("#id_" + Hinst.inst + "_hinst_overlay_labels").val(overlay_labels.join(', '));

      // Hide widget.
      $("#ddgroup").fadeOut(500);
    }
  },


  showMsgBox: function(loc, txt) {
    Hinst.debug && console.log('showMsgBox: loc=' + loc);
    if (loc == '#bulk' && $('#ddgroup #bulk').css('display') == 'none') {
      loc = '';
    }

    $('#ddgroup ' + loc).append('<div class="msgbox"> \
          <div class="msgtext">' + txt + '</div> \
          <div id="msgboxexit"></div> \
        </div>');
    // Close handler.
    $(".msgbox #msgboxexit").click(function(e) {
      $(this).parent().fadeOut();
    });
  },

  hideMsgBox: function(loc) {
    $('#ddgroup ' + loc + ' .msgbox').remove();
  },


  // Identify any empty rows.
  // Return obj with array of row id numbers where img count is 0.
  // Note: if called as part of a drop in bottom row, the img is usually not there yet as can
  // arrive asynchonously, so return array of 'empty' rows and allow caller to check.
  checkEmptyRow: function() {
    var ero = {
      emptyrowa: [],
      lastempty: false, // Not currenty used.
    };
    var icount = 0;
    var r = null;
    $('#ddgroup .row').each(function(ridx) {
      r = $(this).attr('id').substr(1); // r2 -> 2
      icount = $(this).find('.dropzone img').length;
      if (icount == 0) {
        ero.emptyrowa.push(r);
      }
    });
    if (ero.emptyrowa[ero.emptyrowa.length - 1] == r) {
      ero.lastempty = true;
    }
    return ero;
  },


  getLabelFromFilename: function(fn) {
    // nt0600.png or nt150600.png.
    var dotpos = fn.lastIndexOf('.') - fn.length; // -ve.
    return  fn.slice(2, dotpos);
  },

  // Convert rcid (r2c3) into rc obj.
  getRC: function(rcid) {
    var rc = new Object;
    var cpos = rcid.indexOf('c');
    rc.r = rcid.substring(1, cpos); // index-start,index-end
    rc.c = rcid.substring(cpos + 1);
    return rc;
  },

  // Handlers.
  handlers: function() {

    // Drag and drop rows.
    // https://jqueryui.com/sortable
    $("#ddgroup").sortable({
      cursor: "move",
      items: '> .row'
    });

    $("#saveselection").click(function() {
      Hinst.saveSelection();
    });

    // Add row at bottom of list, above 'add' button, when 'add' button clicked.
    $("#addrow").click(function() {
      Hinst.addRow(true);
    });

    // Delete a row when delete button clicked.
    $("#ddgroup .deleterow").click(function(e) {
      Hinst.deleteRow(e);
    });

    // Add column (for an overlay) at right of current right col.
    $("#addcol").click(function() {
      Hinst.addOvCol(true);
    }),
            // Add column (for an overlay) at right of current right col.
            $("#addcolbulk").click(function() {
      Hinst.addColBulk();
    }),
            // Delete a overlay col when delete button clicked.
            $(".deletecol").click(function(e) {
      Hinst.deleteOvCol(e);
    });

    // 'Close' window.
    $("#ddgroup #exit").click(function(e) {
      $("#ddgroup").css('display', 'none');
    });

  }, // end handlers

  dndHandler: function(sel, action) {
    // Set defaults.
    if (typeof sel == 'undefined') {
      sel = '#ddgroup .row .col .dropzone';
    }
    if (typeof action == 'undefined') {
      action = 'both';
    }

    var dropzone = $(sel);
    Hinst.debug && console.log('dndHandler: .dropzone sel=' + sel + ' action=' + action + ' num dzs=' + dropzone.length);

    if (action == 'off' || action == 'both') {
      // Clear any existing.
      dropzone.off('dragover');
      dropzone.off('dragleave');
      dropzone.off('dragenter');
      dropzone.off('drop');
    }

    if (action == 'on' || action == 'both') {
      // Add.
      dropzone.on('dragover', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });

      dropzone.on('dragleave', function(e) {
        e.stopPropagation();
        e.preventDefault();
        $(this).css(Hinst.dragleavestyle);
      });

      dropzone.on('dragenter', function(e) {
        e.stopPropagation();
        e.preventDefault();
        $(this).css(Hinst.dragenterstyle);
      });

      dropzone.on('drop', function(e) {
        e.stopPropagation();
        e.preventDefault();
        Hinst.processDrop(e);
      });
    }

  },

};
