/*******************************************************************************

    uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
    Copyright (C) 2014-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

import { dom, qs$, qsa$ } from './dom.js';
import { browser, localRead, localWrite, sendMessage } from './ext.js';
import { i18n, i18n$ } from './i18n.js';
import punycode from './punycode.js';

/******************************************************************************/

const rulesetMap = new Map();
let cachedRulesetData = {};
let hideUnusedSet = new Set(['regions']);

/******************************************************************************/

function renderNumber(value) {
    return value.toLocaleString();
}

function hashFromIterable(iter) {
    return Array.from(iter).sort().join('\n');
}

/******************************************************************************/

function rulesetStats(rulesetId) {
    const hasOmnipotence = cachedRulesetData.defaultFilteringMode > 1;
    const rulesetDetails = rulesetMap.get(rulesetId);
    if (rulesetDetails === undefined) { return; }
    const { rules, filters } = rulesetDetails;
    let ruleCount = rules.plain + rules.regex;
    if (hasOmnipotence) {
        ruleCount += rules.removeparam + rules.redirect + rules.modifyHeaders;
    }
    const filterCount = filters.accepted;
    return { ruleCount, filterCount };
}

/******************************************************************************/

function renderFilterLists() {
    const { enabledRulesets, rulesetDetails } = cachedRulesetData;
    const listGroupTemplate = qs$('#templates .groupEntry');
    const listEntryTemplate = qs$('#templates .listEntry');
    const listStatsTemplate = i18n$('perRulesetStats');
    const groupNames = new Map([['user', '']]);

    const liFromListEntry = function (ruleset, li, hideUnused) {
        if (!li) {
            li = dom.clone(listEntryTemplate);
        }
        const on = enabledRulesets.includes(ruleset.id);
        dom.cl.toggle(li, 'checked', on);
        dom.cl.toggle(li, 'unused', hideUnused && !on);
        qs$(li, 'input[type="checkbox"]').checked = on;
        if (dom.attr(li, 'data-listkey') !== ruleset.id) {
            dom.attr(li, 'data-listkey', ruleset.id);
            qs$(li, '.listname').append(i18n.patchUnicodeFlags(ruleset.name));
            dom.cl.remove(li, 'toRemove');
            if (ruleset.homeURL) {
                dom.cl.add(li, 'support');
                dom.attr(qs$(li, 'a.support'), 'href', ruleset.homeURL);
            } else {
                dom.cl.remove(li, 'support');
            }
            if (ruleset.instructionURL) {
                dom.cl.add(li, 'mustread');
                dom.attr(qs$(li, 'a.mustread'), 'href', ruleset.instructionURL);
            } else {
                dom.cl.remove(li, 'mustread');
            }
            dom.cl.toggle(li, 'isDefault', ruleset.id === 'default');
        }
        const stats = rulesetStats(ruleset.id);
        li.title = listStatsTemplate
            .replace('{{ruleCount}}', renderNumber(stats.ruleCount))
            .replace('{{filterCount}}', renderNumber(stats.filterCount));
        dom.attr(
            qs$(li, '.input.checkbox'),
            'disabled',
            stats.ruleCount === 0 ? '' : null
        );
        dom.cl.remove(li, 'discard');
        return li;
    };

    const listEntryCountFromGroup = function (groupRulesets) {
        if (Array.isArray(groupRulesets) === false) { return ''; }
        let count = 0,
            total = 0;
        for (const ruleset of groupRulesets) {
            if (enabledRulesets.includes(ruleset.id)) {
                count += 1;
            }
            total += 1;
        }
        return total !== 0 ?
            `(${count.toLocaleString()}/${total.toLocaleString()})` :
            '';
    };

    const liFromListGroup = function (groupKey, groupRulesets) {
        let liGroup = qs$(`#lists > .groupEntry[data-groupkey="${groupKey}"]`);
        if (liGroup === null) {
            liGroup = dom.clone(listGroupTemplate);
            let groupName = groupNames.get(groupKey);
            if (groupName === undefined) {
                groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
                groupNames.set(groupKey, groupName);
            }
            if (groupName !== '') {
                dom.text(qs$(liGroup, '.geName'), groupName);
            }
        }
        if (qs$(liGroup, '.geName:empty') === null) {
            dom.text(
                qs$(liGroup, '.geCount'),
                listEntryCountFromGroup(groupRulesets)
            );
        }
        const hideUnused = mustHideUnusedLists(groupKey);
        dom.cl.toggle(liGroup, 'hideUnused', hideUnused);
        const ulGroup = qs$(liGroup, '.listEntries');
        if (!groupRulesets) { return liGroup; }
        groupRulesets.sort(function (a, b) {
            return (a.name || '').localeCompare(b.name || '');
        });
        for (let i = 0; i < groupRulesets.length; i++) {
            const liEntry = liFromListEntry(
                groupRulesets[i],
                ulGroup.children[i],
                hideUnused
            );
            if (liEntry.parentElement === null) {
                ulGroup.appendChild(liEntry);
            }
        }
        return liGroup;
    };

    // Visually split the filter lists in groups
    const ulLists = qs$('#lists');
    const groups = new Map([
        [
            'default',
            rulesetDetails.filter(ruleset =>
                ruleset.id === 'default'
            ),
        ],
        [
            'annoyances',
            rulesetDetails.filter(ruleset =>
                ruleset.group === 'annoyances'
            ),
        ],
        [
            'misc',
            rulesetDetails.filter(ruleset =>
                ruleset.id !== 'default' &&
                ruleset.group === undefined &&
                typeof ruleset.lang !== 'string'
            ),
        ],
        [
            'regions',
            rulesetDetails.filter(ruleset =>
                typeof ruleset.lang === 'string'
            ),
        ],
    ]);

    dom.cl.toggle(dom.body, 'hideUnused', mustHideUnusedLists('*'));

    for (const [groupKey, groupRulesets] of groups) {
        const liGroup = liFromListGroup(groupKey, groupRulesets);
        dom.attr(liGroup, 'data-groupkey', groupKey);
        if (liGroup.parentElement === null) {
            ulLists.appendChild(liGroup);
        }
    }
}

/******************************************************************************/

function renderWidgets() {
    if (cachedRulesetData.firstRun) {
        dom.cl.add(dom.body, 'firstRun');
    }

    renderDefaultMode();
    renderTrustedSites();

    qs$('#autoReload input[type="checkbox"]').checked = cachedRulesetData.autoReload;

    {
        const input = qs$('#showBlockedCount input[type="checkbox"]');
        if (cachedRulesetData.canShowBlockedCount) {
            input.checked = cachedRulesetData.showBlockedCount;
        } else {
            input.checked = false;
            dom.attr(input, 'disabled', '');
        }
    }

    // Compute total counts
    let rulesetCount = 0;
    let filterCount = 0;
    let ruleCount = 0;
    for (const liEntry of qsa$('#lists .listEntry[data-listkey]')) {
        if (qs$(liEntry, 'input[type="checkbox"]:checked') === null) { continue; }
        rulesetCount += 1;
        const stats = rulesetStats(liEntry.dataset.listkey);
        if (stats === undefined) { continue; }
        ruleCount += stats.ruleCount;
        filterCount += stats.filterCount;
    }
    dom.text('#listsOfBlockedHostsPrompt', i18n$('perRulesetStats')
        .replace('{{ruleCount}}', ruleCount.toLocaleString())
        .replace('{{filterCount}}', filterCount.toLocaleString())
    );

    dom.cl.toggle(dom.body, 'noMoreRuleset',
        rulesetCount === cachedRulesetData.maxNumberOfEnabledRulesets
    );
}

/******************************************************************************/

function renderDefaultMode() {
    const defaultLevel = cachedRulesetData.defaultFilteringMode;
    if (defaultLevel !== 0) {
        qs$(`.filteringModeCard input[type="radio"][value="${defaultLevel}"]`).checked = true;
    } else {
        dom.prop('.filteringModeCard input[type="radio"]', 'checked', false);
    }
}

/******************************************************************************/

async function onFilteringModeChange(ev) {
    const input = ev.target;
    const newLevel = parseInt(input.value, 10);

    switch (newLevel) {
        case 1: { // Revoke broad permissions
            browser.permissions.remove({
                origins: ['<all_urls>']
            });
            cachedRulesetData.defaultFilteringMode = 1;
            break;
        }
        case 2:
        case 3: { // Request broad permissions
            const granted = browser.permissions.request({
                origins: ['<all_urls>']
            });
            if (granted) {
                const actualLevel = sendMessage({
                    what: 'setDefaultFilteringMode',
                    level: newLevel,
                });
                cachedRulesetData.defaultFilteringMode = actualLevel;
            }
            break;
        }
        default:
            break;
    }
    renderFilterLists();
    renderWidgets();
}

dom.on(
    '#defaultFilteringMode',
    'change',
    '.filteringModeCard input[type="radio"]',
    ev => { onFilteringModeChange(ev); }
);

/******************************************************************************/

dom.on('#autoReload input[type="checkbox"]', 'change', ev => {
    sendMessage({
        what: 'setAutoReload',
        state: ev.target.checked,
    });
});

dom.on('#showBlockedCount input[type="checkbox"]', 'change', ev => {
    sendMessage({
        what: 'setShowBlockedCount',
        state: ev.target.checked,
    });
});

/******************************************************************************/

function renderTrustedSites() {
    const textarea = qs$('#trustedSites');
    const hostnames = cachedRulesetData.trustedSites;
    textarea.value = hostnames.map(hn => punycode.toUnicode(hn)).join('\n');
    if (textarea.value !== '') {
        textarea.value += '\n';
    }
}

function changeTrustedSites() {
    const hostnames = getStagedTrustedSites();
    const hash = hashFromIterable(cachedRulesetData.trustedSites);
    if (hashFromIterable(hostnames) === hash) { return; }
    sendMessage({
        what: 'setTrustedSites',
        hostnames,
    });
}

function getStagedTrustedSites() {
    const textarea = qs$('#trustedSites');
    return textarea.value.split(/\s/).map(hn => {
        try {
            return punycode.toASCII(
                (new URL(`https://${hn}/`)).hostname
            );
        } catch (_) {
        }
        return '';
    }).filter(hn => hn !== '');
}

dom.on('#trustedSites', 'blur', changeTrustedSites);

self.addEventListener('beforeunload', changeTrustedSites);

/******************************************************************************/

async function applyEnabledRulesets() {
    const enabledRulesets = [];
    for (const liEntry of qsa$('#lists .listEntry[data-listkey]')) {
        const checked = qs$(liEntry, 'input[type="checkbox"]:checked') !== null;
        dom.cl.toggle(liEntry, 'checked', checked);
        if (checked === false) { continue; }
        enabledRulesets.push(liEntry.dataset.listkey);
    }

    sendMessage({
        what: 'applyRulesets',
        enabledRulesets,
    });

    renderWidgets();
}

dom.on('#lists', 'change', '.listEntry input[type="checkbox"]', () => {
    applyEnabledRulesets();
});

/******************************************************************************/

// Collapsing of unused lists.

function mustHideUnusedLists(which) {
    const hideAll = hideUnusedSet.has('*');
    if (which === '*') { return hideAll; }
    return hideUnusedSet.has(which) !== hideAll;
}

function toggleHideUnusedLists(which) {
    const doesHideAll = hideUnusedSet.has('*');
    let groupSelector;
    let mustHide;
    if (which === '*') {
        mustHide = doesHideAll === false;
        groupSelector = '';
        hideUnusedSet.clear();
        if (mustHide) {
            hideUnusedSet.add(which);
        }
        dom.cl.toggle(dom.body, 'hideUnused', mustHide);
        dom.cl.toggle('.groupEntry[data-groupkey]', 'hideUnused', mustHide);
    } else {
        const doesHide = hideUnusedSet.has(which);
        if (doesHide) {
            hideUnusedSet.delete(which);
        } else {
            hideUnusedSet.add(which);
        }
        mustHide = doesHide === doesHideAll;
        groupSelector = `.groupEntry[data-groupkey="${which}"]`;
        dom.cl.toggle(groupSelector, 'hideUnused', mustHide);
    }

    for (const elem of qsa$(`#lists ${groupSelector} .listEntry[data-listkey] input[type="checkbox"]:not(:checked)`)) {
        dom.cl.toggle(
            elem.closest('.listEntry[data-listkey]'),
            'unused',
            mustHide
        );
    }

    localWrite('hideUnusedFilterLists', Array.from(hideUnusedSet));
}

dom.on('#lists', 'click', '.groupEntry[data-groupkey] > .geDetails', ev => {
    toggleHideUnusedLists(
        dom.attr(ev.target.closest('[data-groupkey]'), 'data-groupkey')
    );
});

// Initialize from saved state.
localRead('hideUnusedFilterLists').then(value => {
    if (Array.isArray(value) === false) { return; }
    hideUnusedSet = new Set(value);
});

/******************************************************************************/

const bc = new self.BroadcastChannel('uBOL');

bc.onmessage = ev => {
    const message = ev.data;
    if (message instanceof Object === false) { return; }
    const local = cachedRulesetData;
    let render = false;

    // Keep added sites which have not yet been committed
    if (message.trustedSites !== undefined) {
        if (hashFromIterable(message.trustedSites) !== hashFromIterable(local.trustedSites)) {
            const current = new Set(local.trustedSites);
            const staged = new Set(getStagedTrustedSites());
            for (const hn of staged) {
                if (current.has(hn) === false) { continue; }
                staged.delete(hn);
            }
            const combined = Array.from(new Set([...message.trustedSites, ...staged]));
            local.trustedSites = combined;
            render = true;
        }
    }

    if (message.defaultFilteringMode !== undefined) {
        if (message.defaultFilteringMode !== local.defaultFilteringMode) {
            local.defaultFilteringMode = message.defaultFilteringMode;
            render = true;
        }
    }

    if (message.autoReload !== undefined) {
        if (message.autoReload !== local.autoReload) {
            local.autoReload = message.autoReload;
            render = true;
        }
    }

    if (message.showBlockedCount !== undefined) {
        if (message.showBlockedCount !== local.showBlockedCount) {
            local.showBlockedCount = message.showBlockedCount;
            render = true;
        }
    }

    if (message.enabledRulesets !== undefined) {
        if (hashFromIterable(message.enabledRulesets) !== hashFromIterable(local.enabledRulesets)) {
            local.enabledRulesets = message.enabledRulesets;
            render = true;
        }
    }

    if (render === false) { return; }
    renderFilterLists();
    renderWidgets();
};

/******************************************************************************/

sendMessage({
    what: 'getOptionsPageData',
}).then(data => {
    if (!data) { return; }
    cachedRulesetData = data;
    rulesetMap.clear();
    cachedRulesetData.rulesetDetails.forEach(rule => rulesetMap.set(rule.id, rule));
    try {
        renderFilterLists();
        renderWidgets();
    } catch (ex) {
    }
}).catch(reason => {
    console.trace(reason);
});

/******************************************************************************/
