// Setup dialog for a new Passpet persona.

function Controller() {
    // Constants.
    const SAMPLE_ADDRESS = 'username@passpet.org';
    const IDLE_TIME = 200; // start work after input is idle for IDLE_TIME ms
    const DUTY_ON = 90; // hash for DUTY_ON ms each cycle
    const DUTY_OFF = 10; // wait for DUTY_OFF ms each cycle
    const MEDIUM_STRENGTH_BITS = 15; // arbitrary definition of "medium"
    const GOOD_STRENGTH_BITS = 30; // arbitrary definition of "good"
    const ATTACKER_HPS = 600000; // a 2006 PC does this many hashes per second
    const ATTACKER_EQUIPMENT = 'a typical $1000 computer made in 2006';
    const PERSONA_WIDTH = 80; // default size of the persona on the toolbar

    // Services.
    const passpet = XPS('@passpet.org/passpet;1', XPI.IPasspet);
    const remote = XPS('@passpet.org/remote-storage;1', XPI.IPasspetRemote);
    const szr = XPS('@passpet.org/serializer;1', XPI.IPasspetSerializer);
    const ee = XPS('@passpet.org/entropy-estimator;1', XPI.IPasspetEntropy);
    const hasher = XPO('@passpet.org/iterated-hash;1', XPI.IPasspetHash);

    // Dialog elements.
    const dialog = document.documentElement;
    const cancelButton = dialog.getButton('cancel');
    const acceptButton = dialog.getButton('accept');
    const deck = $('deck');

    const addressTextbox = $('address-textbox');
    const addressFeedback = $('address-feedback');
    const addressFeedbackDeck = $('address-feedback-deck');
    const addressMessageDeck = $('address-message-deck');
    const useExistingRadio = $('use-existing-radio');
    const useExistingLabel = $('use-existing-label');
    const newSecretRadio = $('new-secret-radio');
    const newSecretLabel = $('new-secret-label');

    const secretTextbox = $('secret-textbox');
    const secretFeedback = $('secret-feedback');
    const secretConfirmTextbox = $('secret-confirm-textbox');
    const secretConfirmFeedback = $('secret-confirm-feedback');
    const attackerTimeSpan = $('attacker-time');
    const attackerEquipmentSpan = $('attacker-equipment');
    const strengthBar = $('strength-bar');

    const buttonImage = $('passpet-buttonimage');
    const personaDeck = $('persona-deck');

    // Instance variables.
    var addressEmpty = true;
    var addressCheckTimeout = null;
    var addressCheckContinuation = null;
    var serverMissingCache = {};
    var addressExistingCache = {};
    var addressNewCache = {};
    var addressExists = false;
    var useExisting = true;
    var entropy = 0;
    var secretHashTimeout = null;
    var page = 0;
    var personaSecret = null;
    var personaAddress = null;
    var personaName = null;
    var personaIconName = null;
    var personaIcon = null;

    // Initialization.
    window.sizeToContent();
    dom.listen(dialog, 'dialogaccept', accept);
    dom.listen(dialog, 'dialogcancel', cancel);
    goto(0);

    // ------------------------------------------------------ page selection

    function goto(newpage) {
        page = newpage;
        dom.set(deck, 'selectedIndex', page);
        switch (page) {
            case 0: initAddressPage(); break;
            case 1: initSecretPage(); break;
            case 2: initPersonaPage(); break;
        }
    };

    function accept(event) {
        print('accept', page);
        var next = null;
        switch (page) {
            case 0: next = finishAddressPage(); break;
            case 1: next = finishSecretPage(); break;
            case 2: next = finishPersonaPage(); break;
        }
        if (next == null) next = page + 1;
        if (next < deck.childNodes.length) {
            goto(next);
            print('advance to', next);
            event.preventDefault();
        }
    };

    function cancel(event) {
        print('cancel');
        return true;
    };

    // -------------------------------------------------- address entry page

    function initAddressPage() {
        acceptButton.label = 'Choose a new master secret';
        dom.disable(acceptButton);
        dom.enable(cancelButton);

        dom.listen(addressTextbox, 'input', addressInput);
        dom.listen(addressTextbox, 'focus', addressFocus);
        dom.listen(addressTextbox, 'blur', addressBlur);
        dom.set(addressFeedbackDeck, 'selectedIndex', -1);
        dom.set(addressMessageDeck, 'selectedIndex', -1);
        addressEmpty = true;
        addressTextbox.focus();
        cancelButton.focus();

        dom.listen(useExistingRadio, 'click', selectUseExisting);
        dom.listen(useExistingLabel, 'click', selectUseExisting);
        dom.listen(newSecretRadio, 'click', selectNewSecret);
        dom.listen(newSecretLabel, 'click', selectNewSecret);
    }

    function selectUseExisting(event) {
        dom.set(useExistingRadio, 'selected', true);
        dom.set(newSecretRadio, 'selected', false);
        acceptButton.label = 'Use my existing account';
        useExisting = true;
    }

    function selectNewSecret(event) {
        dom.set(newSecretRadio, 'selected', true);
        dom.set(useExistingRadio, 'selected', false);
        acceptButton.label = 'Choose a new master secret';
        useExisting = false;
    }

    function addressFocus(event) {
        print('focus event');
        dom.set(addressTextbox, 'sample', 'false');
        if (addressEmpty) {
            addressTextbox.value = '';
        }
    }

    function addressBlur(event) {
        if (addressEmpty) {
            dom.set(addressTextbox, 'sample', 'true');
            addressTextbox.value = SAMPLE_ADDRESS;
        } else {
            dom.set(addressTextbox, 'sample', 'false');
        }
    }

    function validUsername(username) {
        return username.match(/^[a-z0-9_]+$/);
    }

    function validHostname(hostname) {
        if (hostname == 'localhost') return true;
        if (hostname.match(/^([a-z0-9_-]+\.)+[a-z][a-z]+$/)) return true;
        if (hostname.match(/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/)) {
            var bytes = hostname.split('.');
            for (var i = 0; i < bytes.length; i++) {
                if (bytes[i] - 0 > 255) return false;
            }
            return true;
        }
        return false;
    }

    function addressInput(event) {
        clearTimeout(addressCheckTimeout);
        if (addressCheckContinuation) {
            addressCheckContinuation.cancel();
        }

        var address = addressTextbox.value.toLowerCase().replace(/ /g, '');
        addressTextbox.value = address;
        var parts = address.split('@'), ready = false;
        var feedbackPage = -1, messagePage = -1;

        addressEmpty = (address == '');
        if (addressEmpty) {
            dom.set(addressFeedback, 'status', 'blank');
            dom.put(addressFeedback, '');
        } else if (parts.length != 2 || !parts[0] || !parts[1]) {
            feedbackPage = 0;
            dom.set(addressFeedback, 'status', 'invalid');
            dom.put(addressFeedback, 'invalid address');
        } else if (!validUsername(parts[0])) {
            feedbackPage = 0;
            messagePage = 0;
            dom.set(addressFeedback, 'status', 'invalid');
            dom.put(addressFeedback, 'invalid username');
        } else if (!validHostname(parts[1])) {
            feedbackPage = 0;
            messagePage = 1;
            dom.set(addressFeedback, 'status', 'invalid');
            dom.put(addressFeedback, 'invalid hostname');
        } else {
            if (parts[1] in serverMissingCache) {
                feedbackPage = 0;
                dom.set(addressFeedback, 'status', 'no-server');
                dom.put(addressFeedback, 'no such server');
            } else if (address in addressExistingCache) {
                feedbackPage = 0;
                messagePage = 2;
                addressExists = true;
                ready = true;
                dom.set(addressFeedback, 'status', 'existing');
                dom.put(addressFeedback, 'existing account');
                selectUseExisting();
            } else if (address in addressNewCache) {
                feedbackPage = 0;
                messagePage = 3;
                addressExists = false;
                ready = true;
                dom.set(addressFeedback, 'status', 'new');
                dom.put(addressFeedback, 'new account');
                selectNewSecret();
            } else {
                feedbackPage = 1;
                addressCheckTimeout = setTimeout(function() {
                    checkAddress(parts[0], parts[1]);
                }, IDLE_TIME);
            }
        }
        dom.set(addressFeedbackDeck, 'selectedIndex', feedbackPage);
        dom.set(addressMessageDeck, 'selectedIndex', messagePage);
        dom.enable(acceptButton, ready);
    }

    function checkAddress(username, hostname) {
        var address = username + '@' + hostname;

        addressCheckContinuation = new Cont('checkAddress1',
            function(result) {
                if (result) {
                    addressExistingCache[address] = true;
                    messagePage = 2;
                    addressExists = true;
                    dom.set(addressFeedback, 'status', 'existing');
                    dom.put(addressFeedback, 'existing account');
                    selectUseExisting();
                } else {
                    addressNewCache[address] = true;
                    messagePage = 3;
                    addressExists = false;
                    dom.set(addressFeedback, 'status', 'new');
                    dom.put(addressFeedback, 'new account');
                    selectNewSecret();
                }
                dom.enable(acceptButton);
                dom.set(addressFeedbackDeck, 'selectedIndex', 0);
                dom.set(addressMessageDeck, 'selectedIndex', messagePage);
            },
            function(error) {
                serverMissingCache[hostname] = true;
                dom.set(addressFeedback, 'status', 'no-server');
                dom.put(addressFeedback, 'no such server');
                dom.set(addressFeedbackDeck, 'selectedIndex', 0);
                dom.set(addressMessageDeck, 'selectedIndex', -1);
            }
        );
        remote.list(address, addressCheckContinuation);
    }

    function finishAddressPage() {
        personaAddress = addressTextbox.value;
        if (addressExists && useExisting) return 2;
    }

    // ----------------------------------------------- secret selection page

    function initSecretPage() {
        acceptButton.label = 'Use this master secret';
        dom.disable(acceptButton);
        dom.enable(cancelButton);

        dom.put(attackerEquipmentSpan, ATTACKER_EQUIPMENT);
        updateEntropy();
        updateAttackerTime();
        dom.listen(secretTextbox, 'input', secretInput);
        dom.listen(secretConfirmTextbox, 'input', secretConfirmInput);
        secretTextbox.focus();
    }

    function floor(number, decimals) {
        var unit = Math.pow(0.1, decimals)
        return (Math.floor(number/unit)*unit).toFixed(decimals)
    }

    function updateAttackerTime() {
        var guesses = Math.pow(2, entropy) / 2;
        var days = hasher.iterations*guesses / (86400.0*ATTACKER_HPS);
        var timeSpec;

        // These are carefully chosen to avoid grammar errors like "1 months". 
        if (days < 1) timeSpec = floor(days*24, 1) + ' hours';
        else if (days < 10) timeSpec = floor(days, 1) + ' days';
        else if (days < 30.44) timeSpec = floor(days, 0) + ' days';
        else if (days < 182.64) timeSpec = floor(days/30.44, 1) + ' months';
        else if (days < 365.24) timeSpec = floor(days/30.44, 0) + ' months';
        else if (days < 1826.4) timeSpec = floor(days/365.24, 1) + ' years';
        else timeSpec = floor(days/365.24, 0) + ' years';
        dom.put(attackerTimeSpan, timeSpec);

        // The strength meter is drawn so that a factor of 1.1 is 3 pixels.
        var pixels = Math.round(Math.log(days)/Math.log(1.1)*3) + 1;
        pixels = Math.min(Math.max(pixels, 0), 404);
        strengthBar.style.width = pixels + 'px';
    }

    function updateEntropy() {
        entropy = Math.floor(ee.getPasswordEntropyBits(secretTextbox.value));
        var level = 'weak';
        if (entropy > MEDIUM_STRENGTH_BITS) level = 'medium';
        if (entropy > GOOD_STRENGTH_BITS) level = 'good';
        var amount = entropy == 1 ? '1 bit' : entropy + ' bits';
        dom.set(secretFeedback, 'level', level);
        dom.put(secretFeedback, level + ' (' + amount + ')');
    };

    function updateSecretConfirm() {
        if (secretTextbox.value &&
            secretConfirmTextbox.value == secretTextbox.value) {
            dom.set(secretConfirmFeedback, 'match', 'true');
            dom.put(secretConfirmFeedback, 'confirmed!');
            dom.enable(acceptButton);
        } else {
            dom.set(secretConfirmFeedback, 'match', 'false');
            dom.put(secretConfirmFeedback, '');
            dom.disable(acceptButton);
        }
    };

    function hashSecret() { 
        hasher.run(-1, DUTY_ON);
        updateAttackerTime();
        secretHashTimeout = setTimeout(hashSecret, DUTY_OFF);
    }

    function secretInput(event) {
        clearTimeout(secretHashTimeout);
        var secretu8 = szr.toUTF8(secretTextbox.value);
        hasher.data = addressTextbox.value + '\0' + secretu8;
        updateEntropy();
        updateSecretConfirm();
        updateAttackerTime();
        secretHashTimeout = setTimeout(hashSecret, IDLE_TIME);
    }

    function secretConfirmInput(event) {
        updateSecretConfirm();
    }

    function finishSecretPage() {
        personaSecret = secretTextbox.value;
    }

    // ------------------------------------------- persona introduction page

    function initPersonaPage() {
        acceptButton.label = 'OK';
        dom.disable(acceptButton);
        dom.enable(cancelButton);
        dom.set(personaDeck, 'selectedIndex', 0);

        personaName = randomChoice(names);
        var iconNames = [];
        for (var iconName in icons) iconNames.push(iconName);
        personaIconName = randomChoice(iconNames);
        personaIcon = icons[personaIconName];

        map(dom.tags('span', 'persona-name'), function(span) {
            dom.put(span, personaName);
        });
        map(dom.tags('span', 'persona-icon'), function(span) {
            dom.put(span, personaIconName);
        });
        dom.set(buttonImage, 'src', personaIcon);

        print('finish');
        var k1 = hasher.iterations, V = hasher.data;
        print('secret', personaSecret);
        print('k1', k1);
        print('V', szr.toHex(V));
        var pid = passpet.createPersona(personaAddress, personaName,
                                        personaIconName, personaIcon,
                                        PERSONA_WIDTH);
        var persona = passpet.getPersona(pid);
        if (addressExists && useExisting) {
            persona.save(true);
            announceSuccess();
        } else {
            persona.setup(personaSecret, k1, V, new Cont('initPersonaPage1',
                function(result) {
                    print('setup complete');
                    announceSuccess();
                },
                function(error) {
                    announceFailure();
                }
            ));
        }
    }

    function announceSuccess() {
        print('announceSuccess');
        dom.set(personaDeck, 'selectedIndex', 1);
        dom.enable(acceptButton);
        dom.hide(cancelButton);
    }

    function announceFailure() {
        print('announceFailure');
        dom.set(personaDeck, 'selectedIndex', 2);
        dom.hide(acceptButton);
        dom.disable(acceptButton);
        dom.show(cancelButton);
        dom.enable(cancelButton);
    }

    function finishPersonaPage() {
    }
};

window.addEventListener('load', function() { new Controller(); }, false);

