/***********************************************************************
* retro-220/webUI B220SystemConfig.js
************************************************************************
* Copyright (c) 2017, Paul Kimpel.
* Licensed under the MIT License, see
*       http://www.opensource.org/licenses/mit-license.php
************************************************************************
* Burroughs 220 Emulator System Configuration management object.
*
* Defines the system configuration used internally by the emulator and the
* methods used to manage that configuration data.
*
************************************************************************
* 2017-01-01  P.Kimpel
*   Original version, from retro-205 webUI/D205SystemConfig.js.
***********************************************************************/
"use strict";

/**************************************/
function B220SystemConfig() {
    /* Constructor for the SystemConfig configuration management object */
    var s;

    this.flushTimer = 0;                // timer token for flushing configuration to localStorage
    this.window = null;                 // configuration UI window object
    this.alertWin = window;             // current base window for alert/confirm/prompt

    // Load or create the system configuration data
    s = localStorage.getItem(this.configStorageName);
    if (!s) {
        this.createConfigData();
    } else {
        this.loadConfigData(s);
    }
}

/**************************************/
B220SystemConfig.prototype.configStorageName = "retro-220-Config";
B220SystemConfig.prototype.configVersion = 1;
B220SystemConfig.prototype.flushDelay = 60000;  // flush timer setting, ms

B220SystemConfig.defaultConfig = {
    version: this.configVersion,
    memorySize: 5000,                   // 11-digit words

    ControlConsole: {
        PCS1SW: 0,                      // Program Control Switches 1-0
        PCS2SW: 0,
        PCS3SW: 0,
        PCS4SW: 0,
        PCS5SW: 0,
        PCW6SW: 0,
        PCS7SW: 0,
        PCS8SW: 0,
        PCS9SW: 0,
        PCS0SW: 0,
        SONSW: 0,                       // S-register on
        SUNITSSW: 0,                    // S-register units
        STOCSW: 0,                      // S-to-C stop
        STOPSW: 0},                     // S-to-P stop

    ConsoleInput: {
        units: [
            null,                       // unit[0] not used
            {type: "PTRA", unitMask: 0x002, remote: 0, speed: 0},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"}
            ]},

    ConsoleOutput: {
        units: [
            {type: "TTYA", unitMask: 0x001, remote: 1, format: 0, zeroSuppress: 0, mapMemory: 0, printerSpeed: 0,
                           columns: 72, tabs: "9,17,25,33,41,49,57,65,73,81"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"}
            ]},

    Cardatron: {
        hasCardatron: true,
        units: [
            null,                       // unit[0] not used
            {type: "CR1", formatSelect: 0, formatCol: 1},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "LP2", algolGlyphs: true, greenBar: true,  zeroSuppressCols: ""},
            {type: "CP1", algolGlyphs: true, greenBar: false, zeroSuppressCols: ""}
            ]},

    MagTape: {
        hasMagTape: true,
        units: [
            null,                       // unit[0] not used
            {type: "MTA",  designate: 0},
            {type: "MTB",  designate: 1},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"},
            {type: "NONE"}
            ]}
    };


/**************************************/
B220SystemConfig.prototype.$$ = function $$(id) {
    return this.doc.getElementById(id);
}

/**************************************/
B220SystemConfig.prototype.createConfigData = function createConfigData() {
    /* Creates and initializes a new configuration data object and stores it in
    localStorage. If former state preference objects exist, these are merged into
    the new data object and then deleted */
    var pref;
    var prefs;
    var s;

    this.configData = B220SystemConfig.defaultConfig;
    this.flushHandler();
};

/**************************************/
B220SystemConfig.prototype.loadConfigData = function loadConfigData(jsonConfig) {
    /* Attempts to parse the JSON configuration data string and store it in
    this.configData. If the parse is unsuccessful, recreates the default configuration.
    Applies any necessary updates to older configurations */

    try {
        this.configData = JSON.parse(jsonConfig);
    } catch (e) {
        alert("Could not parse system configuration data:\n" +
              e.message + "\nReinitializing configuration");
        this.createConfigData();
    }

    // Apply updates
    if (this.getNode("Cardatron.hasCardatron") === undefined) {
        this.putNode("Cardatron.hasCardatron", true);
    }

    if (this.getNode("MagTape.hasMagTape") === undefined) {
        this.putNode("MagTape.hasMagTape", true);
    }
};

/**************************************/
B220SystemConfig.prototype.flushHandler = function flushHandler() {
    /* Callback function for the flush timer. Stores the configuration data */

    this.flushTimer = 0;
    localStorage.setItem(this.configStorageName, JSON.stringify(this.configData));
};

/*************************************/
B220SystemConfig.prototype.flush = function flush() {
    /* If the current configuration data object has been modified, stores it to
    localStorage and resets the flush timer */

    if (this.flushTimer) {
        clearCallback(this.flushTimer);
        this.flushHandler();
    }
};

/**************************************/
B220SystemConfig.prototype.getNode = function getNode(nodeName, index) {
    /* Retrieves a specified node of the configuration data object tree.
    "nodeName" specifies the node using dotted-path format. A blank name
    retrieves the entire tree. If the "index" parameter is specified, the final
    node in the path is assumed to be an array, and "index" used to return
    that element of the array. If a node does not exist, returns undefined */
    var name;
    var names;
    var node = this.configData;
    var top;
    var x;

    name = nodeName.trim();
    if (name.length > 0) {
        names = name.split(".");
        top = names.length;
        for (x=0; x<top; ++x) {
            name = names[x];
            if (name in node) {
                node = node[name];
            } else {
                node = undefined;
                break; // out of for loop
            }
        } // for x
    }

    if (index === undefined) {
        return node;
    } else {
        return node[index];
    }
};

/**************************************/
B220SystemConfig.prototype.putNode = function putNode(nodeName, data, index) {
    /* Creates or replace a specified node of the configuration data object tree.
    "nodeName" specifies the node using dotted.path format. A blank name
    results in nothing being set. If a node does not exist, it and any necessary
    parent nodes are created. If the "index" parameter is specified, the final
    node in the path is assumed to be an array, and "index" is used to access
    that element of the array. Setting the value of a node starts a timer  (if it
    is not already started). When that timer expires, the configuration data will
    be flushed to the localStorage object. This delayed storage is done so that
    several configuration changes into short order can be grouped in one flush */
    var name;
    var names;
    var node = this.configData;
    var top;
    var x;

    name = nodeName.trim();
    if (name.length > 0) {
        names = name.split(".");
        top = names.length-1;
        for (x=0; x<top; ++x) {
            name = names[x];
            if (name in node) {
                node = node[name];
            } else {
                node = node[name] = {};
            }
        } // for x

        if (index === undefined) {
            node[names[top]] = data;
        } else {
            node[names[top]][index] = data;
        }

        if (!this.flushTimer) {
            this.flushTimer = setCallback("CONFIG", this, this.flushTimeout, this.flushHandler);
        }
    }
};

/***********************************************************************
*   System Configuration UI Support                                    *
***********************************************************************/

/**************************************/
B220SystemConfig.prototype.setListValue = function setListValue(id, value) {
    /* Sets the selection of the <select> list with the specified "id" to the
    entry with the specified "value". If no such value exists, the list
    selection is not changed */
    var e = this.$$(id);
    var opt;
    var x;

    if (e && e.tagName == "SELECT") {
        opt = e.options;
        for (x=0; x<opt.length; ++x) {
            if (opt[x].value == value) {
                e.selectedIndex = x;
                break; // out of for loop
            }
        } // for x
    }
};

/**************************************/
B220SystemConfig.prototype.loadConfigDialog = function loadConfigDialog() {
    /* Loads the configuration UI window with the settings from this.configData */
    var cd = this.configData;           // local configuration reference
    var mask;                           // unit mask bits
    var prefix;                         // unit id prefix
    var unit;                           // unit configuration object
    var x;                              // unit index
    var y;                              // secondary index

    // System Properties
    this.setListValue("SystemMemorySize", cd.memorySize.toString());

    // Console Input units

    if (!cd.ConsoleInput) {
        cd.ConsoleInput = B220SystemConfig.defaultConfig.ConsoleInput;
    }

    for (x=1; x<=10; ++x) {
        unit = cd.ConsoleInput.units[x];
        prefix = "ConsoleIn" + x;
        this.setListValue(prefix + "Type", unit.type);
        mask = 0x001;
        for (y=1; y<=10; ++y) {
            mask <<= 1;
            this.$$(prefix + "_" + y).checked = (unit.unitMask & mask ? true : false);
        } // for y
    } // for x

    // Console Output units

    if (!cd.ConsoleOutput) {
        cd.ConsoleOutput = B220SystemConfig.defaultConfig.ConsoleOutput;
    }

    for (x=0; x<=10; ++x) {
        unit = cd.ConsoleOutput.units[x];
        prefix = "ConsoleOut" + x;
        this.setListValue(prefix + "Type", unit.type);
        mask = 0x001;
        this.$$(prefix + "_SPO").checked = (unit.unitMask & mask ? true : false);
        for (y=1; y<=10; ++y) {
            mask <<= 1;
            this.$$(prefix + "_" + y).checked = (unit.unitMask & mask ? true : false);
        } // for y

        this.setListValue(prefix + "Format", unit.format);
        this.$$(prefix + "Columns").textContent = (unit.columns ? unit.columns : 72);
        this.$$(prefix + "Tabs").textContent = (unit.tabs ? unit.tabs : "");
    } // for x

    // Cardatron units

    if (!cd.Cardatron) {
        cd.Cardatron = B220SystemConfig.defaultConfig.Cardatron;
    }

    for (x=1; x<=7; ++x) {
        unit = cd.Cardatron.units[x];
        prefix = "Cardatron" + x;
        this.setListValue(prefix + "Type", unit.type);
        switch (unit.type.substring(0, 2)) {
        case "LP":
            this.$$(prefix + "AlgolGlyphs").checked = unit.algolGlyphs;
            this.$$(prefix + "Greenbar").checked = unit.greenBar;
            this.$$(prefix + "ZeroSuppressCols").textContent = unit.zeroSuppressCols || "";
            break;
        case "CP":
            this.$$(prefix + "AlgolGlyphs").checked = unit.algolGlyphs;
            this.$$(prefix + "Greenbar").checked = false;
            this.$$(prefix + "ZeroSuppressCols").textContent = unit.zeroSuppressCols || "";
            break;
        case "CR":
        default:
            this.$$(prefix + "AlgolGlyphs").checked = false;
            this.$$(prefix + "Greenbar").checked = false;
            this.$$(prefix + "ZeroSuppressCols").textContent = "";
            break;
        } // switch unit.type
    } // for x

    // Magnetic Tape units

    if (!cd.MagTape) {
        cd.MagTape = B220SystemConfig.defaultConfig.MagTape;
    }

    for (x=1; x<=10; ++x) {
        unit = cd.MagTape.units[x];
        prefix = "MagTape" + x;
        this.setListValue(prefix + "Type", unit.type);
        this.$$(prefix + "Designate").selectedIndex = unit.designate-1;
    } // for x

    this.$$("MessageArea").textContent = "220 System Configuration loaded.";
    this.window.focus();
};

/**************************************/
B220SystemConfig.prototype.saveConfigDialog = function saveConfigDialog() {
    /* Saves the configuration UI window settings to this.configData and flushes
    the updated configuration to localStorage */
    var cd = this.configData;           // local configuration reference
    var e;                              // local element reference
    var mask;                           // unit mask
    var prefix;                         // unit id prefix
    var unit;                           // unit configuration object
    var x;                              // unit index
    var y;                              // secondary index

    function getNumber(id, caption, min, max) {
        var n;
        var text = this.$$(id).value;

        n = parseInt(text, 10);
        if (isNaN(n)) {
            alert(caption + " must be numeric");
        } else if (n < min || n > max) {
            alert(caption + " must be in the range (" + min + ", " + max + ")");
            n = Number.NaN;
        }

        return n;
    }

    // System Properties

    e = this.$$("SystemMemorySize");
    x = parseInt(e.options[e.selectedIndex].value, 10);
    cd.memorySize = (isNaN(x) ? 5000 : x);

    // Console Input units

    for (x=1; x<=10; ++x) {
        unit = cd.ConsoleInput.units[x];
        prefix = "ConsoleIn" + x;
        e = this.$$(prefix + "Type");
        unit.type = (e.selectedIndex < 0 ? "NONE" : e.options[e.selectedIndex].value);
        mask = 0x001;
        unit.unitMask = 0;
        for (y=1; y<=10; ++y) {
            mask <<= 1;
            if (this.$$(prefix + "_" + y).checked) {
                unit.unitMask |= mask;
            }
        } // for y

        if (unit.type != "NONE") {
            unit.remote = (unit.remote || 0);
            unit.speed = (unit.speed || 0);
        }
    } // for x

    // Console Output units

    for (x=0; x<=10; ++x) {
        unit = cd.ConsoleOutput.units[x];
        prefix = "ConsoleOut" + x;
        e = this.$$(prefix + "Type");
        unit.type = (e.selectedIndex < 0 ? "NONE" : e.options[e.selectedIndex].value);
        mask = 0x001;
        unit.unitMask = 0;
        if (this.$$(prefix + "_SPO").checked) {
            unit.unitMask |= mask;
        }
        for (y=1; y<=10; ++y) {
            mask <<= 1;
            if (this.$$(prefix + "_" + y).checked) {
                unit.unitMask |= mask;
            }
        } // for y

        if (unit.type != "NONE") {
            unit.remote = (unit.remote || 0);
            unit.zeroSuppress = (unit.zeroSuppress || 0);
            unit.mapMemory = (unit.mapMemory || 0);
            unit.printerSpeed = (unit.printerSpeed || 0);
            e = this.$$(prefix + "Format");
            unit.format = (e.selectedIndex < 0 ? "NONE" : e.options[e.selectedIndex].value);
            unit.columns = (unit.columns ? unit.columns : 72);
            unit.tabs = (unit.tabs ? unit.tabs : "1,9,17,25,33,41,49,57,65,73");
        }
    } // for x

    // Cardatron units

    cd.Cardatron.hasCardatron = false;
    for (x=1; x<=7; ++x) {
        unit = cd.Cardatron.units[x];
        prefix = "Cardatron" + x;
        e = this.$$(prefix + "Type");
        unit.type = (e.selectedIndex < 0 ? "NONE" : e.options[e.selectedIndex].value);
        switch (unit.type.substring(0, 2)) {
        case "LP":
            cd.Cardatron.hasCardatron = true;
            unit.algolGlyphs = this.$$(prefix + "AlgolGlyphs").checked;
            unit.greenBar = this.$$(prefix + "Greenbar").checked;
            unit.zeroSuppressCols = (unit.zeroSuppressCols || "");
            break;
        case "CP":
            cd.Cardatron.hasCardatron = true;
            unit.algolGlyphs = this.$$(prefix + "AlgolGlyphs").checked;
            unit.greenBar = false;
            unit.zeroSuppressCols = (unit.zeroSuppressCols || "");
            break;
        case "CR":
            cd.Cardatron.hasCardatron = true;
            unit.formatSelect = (unit.formatSelect || 0);
            unit.formatCol = (unit.formatCol === undefined ? 1 : unit.formatCol)
            // no break
        default:
            unit.algolGlyphs = false;
            unit.greenBar = false;
            break;
        } // switch unit.type
    } // for x



    // Magnetic Tape units

    cd.MagTape.hasMagTape = false;

    for (x=1; x<=10; ++x) {
        unit = cd.MagTape.units[x];
        prefix = "MagTape" + x;
        e = this.$$(prefix + "Type");
        unit.type = (e.selectedIndex < 0 ? "NONE" : e.options[e.selectedIndex].value);
        unit.designate = this.$$(prefix + "Designate").selectedIndex+1;
        if (unit.type != "NONE") {
            cd.MagTape.hasMagTape = true;
        }
    } // for x

    this.flushHandler();                // store the configuration
    this.$$("MessageArea").textContent = "220 System Configuration updated.";
    this.window.close();
};

/**************************************/
B220SystemConfig.prototype.closeConfigUI = function closeConfigUI() {
    /* Closes the system configuration update dialog */

    this.alertWin = window;             // revert alerts to the global window
    window.focus();
    if (this.window) {
        if (!this.window.closed) {
            this.window.close();
        }
        this.window = null;
    }
}

/**************************************/
B220SystemConfig.prototype.openConfigUI = function openConfigUI() {
    /* Opens the system configuration update dialog and displays the current
    system configuration */

    function configUI_Load(ev) {
        this.doc = ev.target;
        this.window = this.doc.defaultView;
        this.window.moveTo(screen.availWidth-this.window.outerWidth-40,
                (screen.availHeight-this.window.outerHeight)/2);
        this.window.focus();
        this.alertWin = this.window;
        this.$$("SaveBtn").addEventListener("click",
                this.saveConfigDialog.bind(this), false);
        this.$$("CancelBtn").addEventListener("click",
                function(ev) {this.window.close()}.bind(this), false);
        this.$$("DefaultsBtn").addEventListener("click",
                function(ev) {
                    this.createConfigData();
                    this.loadConfigDialog();
        }.bind(this), false);
        this.window.addEventListener("unload",
                this.closeConfigUI.bind(this), false);
        this.loadConfigDialog();
    }

    this.doc = null;
    this.window = null;
    B220Util.openPopup(window, "../webUI/B220SystemConfig.html", this.configStorageName,
            "location=no,scrollbars,resizable,width=800,height=800",
            this, configUI_Load);
};
