var MAX_NAME_LENGTH = 40;

var SHIPPED = [
    {
        id: "moon",
        name: "Moon",
        scheme: "dark",
        accent: "#82aaff",
        secondary: "#b172b0",
        shipped: true
    },
    {
        id: "moon-rose",
        name: "Moon Rose",
        scheme: "dark",
        accent: "#ff757f",
        secondary: "#c099ff",
        shipped: true
    },
    {
        id: "day",
        name: "Day",
        scheme: "light",
        accent: "#2e7de9",
        secondary: "#9854f1",
        shipped: true
    }
];

// The curated accents. `gnome` is the nearest member of GNOME's own
// accent-color enum, which is a fixed list of nine we do not get to extend. It
// is what libadwaita applications -- Files, Papers, Loupe -- are told to use, so
// choosing an accent here recolors them too instead of leaving them in GNOME
// blue. Nearest by hue, not by name: "rose" maps to red rather than pink
// because it is the red role in this palette. adwaita-accent-contract reads
// this table and fails when a member is missing or is not in GNOME's enum.
var CURATED = {
    blue: {
        dark: "#82aaff", darkSecondary: "#b172b0",
        light: "#2e7de9", lightSecondary: "#9854f1",
        label: "Prism blue",
        gnome: "blue"
    },
    orchid: {
        dark: "#c099ff", darkSecondary: "#fca7ea",
        light: "#7847bd", lightSecondary: "#9854f1",
        label: "Orchid",
        gnome: "purple"
    },
    teal: {
        dark: "#86e1fc", darkSecondary: "#82aaff",
        light: "#007197", lightSecondary: "#2e7de9",
        label: "Teal",
        gnome: "teal"
    },
    green: {
        dark: "#c3e88d", darkSecondary: "#86e1fc",
        light: "#587539", lightSecondary: "#007197",
        label: "Green",
        gnome: "green"
    },
    amber: {
        dark: "#ffc777", darkSecondary: "#ff966c",
        light: "#8c6c3e", lightSecondary: "#b15c00",
        label: "Amber",
        gnome: "yellow"
    },
    orange: {
        dark: "#ff966c", darkSecondary: "#ff757f",
        light: "#b15c00", lightSecondary: "#c64343",
        label: "Orange",
        gnome: "orange"
    },
    rose: {
        dark: "#ff757f", darkSecondary: "#c099ff",
        light: "#f52a65", lightSecondary: "#9854f1",
        label: "Rose",
        gnome: "red"
    },
    slate: {
        dark: "#828bb8", darkSecondary: "#82aaff",
        light: "#6172b0", lightSecondary: "#2e7de9",
        label: "Slate",
        gnome: "slate"
    }
};

function copyProfile(profile) {
    return {
        id: profile.id,
        name: profile.name,
        scheme: profile.scheme,
        accent: profile.accent,
        secondary: profile.secondary,
        shipped: profile.shipped === true
    };
}

function shippedProfiles() {
    return SHIPPED.map(copyProfile);
}

function curatedAccents() {
    var result = {};
    Object.keys(CURATED).forEach(function(name) {
        result[name] = Object.assign({}, CURATED[name]);
    });
    return result;
}

function isScheme(value) {
    return value === "dark" || value === "light";
}

function normalizedColor(value) {
    var color = String(value || "").trim().toLowerCase();
    return /^#[0-9a-f]{6}$/.test(color) ? color : null;
}

function normalizeStoredProfile(value) {
    if (!value || typeof value !== "object" || value.shipped === true)
        return null;

    var id = String(value.id || "").trim();
    var name = String(value.name || "").trim();
    var accent = normalizedColor(value.accent);
    var secondary = normalizedColor(value.secondary);
    if (!/^custom-[a-z0-9][a-z0-9-]{0,56}$/.test(id)
            || name.length === 0 || name.length > MAX_NAME_LENGTH
            || !isScheme(value.scheme) || !accent || !secondary)
        return null;

    return {
        id: id,
        name: name,
        scheme: value.scheme,
        accent: accent,
        secondary: secondary,
        shipped: false
    };
}

function validCustomProfiles(values) {
    if (!Array.isArray(values))
        return [];

    var ids = {};
    var names = {};
    SHIPPED.forEach(function(profile) {
        ids[profile.id] = true;
        names[profile.name.toLowerCase()] = true;
    });

    var result = [];
    values.forEach(function(value) {
        var profile = normalizeStoredProfile(value);
        if (!profile)
            return;
        var foldedName = profile.name.toLowerCase();
        if (ids[profile.id] || names[foldedName])
            return;
        ids[profile.id] = true;
        names[foldedName] = true;
        result.push(profile);
    });
    return result;
}

function profileCatalog(values) {
    return shippedProfiles().concat(validCustomProfiles(values));
}

function boundedName(value) {
    var name = String(value || "").trim();
    if (!name)
        name = "Custom theme";
    return name.slice(0, MAX_NAME_LENGTH);
}

function uniqueName(value, profiles) {
    var requested = boundedName(value);
    var names = {};
    profiles.forEach(function(profile) {
        names[profile.name.toLowerCase()] = true;
    });
    if (!names[requested.toLowerCase()])
        return requested;

    for (var suffix = 2; suffix < 10000; suffix++) {
        var ending = " " + suffix;
        var candidate = requested.slice(0, MAX_NAME_LENGTH - ending.length) + ending;
        if (!names[candidate.toLowerCase()])
            return candidate;
    }
    return requested.slice(0, MAX_NAME_LENGTH - 6) + " 10000";
}

function slug(value) {
    var result = String(value || "").toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "")
        .slice(0, 48)
        .replace(/-+$/g, "");
    return result || "theme";
}

function uniqueId(name, profiles) {
    var base = "custom-" + slug(name);
    var ids = {};
    profiles.forEach(function(profile) { ids[profile.id] = true; });
    if (!ids[base])
        return base;
    for (var suffix = 2; suffix < 10000; suffix++) {
        var candidate = base.slice(0, 57 - String(suffix).length) + "-" + suffix;
        if (!ids[candidate])
            return candidate;
    }
    return base.slice(0, 52) + "-10000";
}

function createCustomProfile(values, input) {
    var customs = validCustomProfiles(values);
    var catalog = shippedProfiles().concat(customs);
    var scheme = input && input.scheme;
    var accent = normalizedColor(input && input.accent);
    var secondary = normalizedColor(input && input.secondary);
    if (!isScheme(scheme) || !accent || !secondary)
        return { profiles: customs, profile: null };

    var name = uniqueName(input && input.name, catalog);
    var profile = {
        id: uniqueId(name, catalog),
        name: name,
        scheme: scheme,
        accent: accent,
        secondary: secondary,
        shipped: false
    };
    return { profiles: customs.concat([profile]), profile: profile };
}

function findProfile(values, id) {
    var catalog = profileCatalog(values);
    for (var index = 0; index < catalog.length; index++) {
        if (catalog[index].id === id)
            return catalog[index];
    }
    return null;
}

function editProfile(values, selected, changes) {
    var customs = validCustomProfiles(values);
    if (!selected || typeof selected !== "object")
        return { profiles: customs, profile: null };

    var accent = normalizedColor(changes && changes.accent !== undefined
        ? changes.accent : selected.accent);
    var secondary = normalizedColor(changes && changes.secondary !== undefined
        ? changes.secondary : selected.secondary);
    var scheme = changes && changes.scheme !== undefined ? changes.scheme : selected.scheme;
    if (!isScheme(scheme) || !accent || !secondary)
        return { profiles: customs, profile: null };

    if (selected.shipped === true) {
        return createCustomProfile(customs, {
            name: selected.name + " custom",
            scheme: scheme,
            accent: accent,
            secondary: secondary
        });
    }

    var stored = normalizeStoredProfile(selected);
    if (!stored)
        return { profiles: customs, profile: null };

    var updated = Object.assign({}, stored, {
        scheme: scheme,
        accent: accent,
        secondary: secondary
    });
    var found = false;
    var next = customs.map(function(profile) {
        if (profile.id !== updated.id)
            return profile;
        found = true;
        return updated;
    });
    if (!found)
        next.push(updated);
    return { profiles: next, profile: updated };
}

function deleteProfile(values, id) {
    var customs = validCustomProfiles(values);
    for (var shippedIndex = 0; shippedIndex < SHIPPED.length; shippedIndex++) {
        if (SHIPPED[shippedIndex].id === id)
            return { profiles: customs, removed: false };
    }
    var next = customs.filter(function(profile) { return profile.id !== id; });
    return { profiles: next, removed: next.length !== customs.length };
}

function curatedPair(name, scheme) {
    var entry = CURATED[name] || CURATED.blue;
    if (scheme === "light")
        return { accent: entry.light, secondary: entry.lightSecondary };
    return { accent: entry.dark, secondary: entry.darkSecondary };
}

function matchingShippedProfile(scheme, accent, secondary) {
    var first = normalizedColor(accent);
    var second = normalizedColor(secondary);
    for (var index = 0; index < SHIPPED.length; index++) {
        var profile = SHIPPED[index];
        if (profile.scheme === scheme && profile.accent === first && profile.secondary === second)
            return copyProfile(profile);
    }
    return null;
}

function curatedNameForProfile(profile) {
    if (!profile || !isScheme(profile.scheme))
        return "";
    var accent = normalizedColor(profile.accent);
    var secondary = normalizedColor(profile.secondary);
    var names = Object.keys(CURATED);
    for (var index = 0; index < names.length; index++) {
        var name = names[index];
        var pair = curatedPair(name, profile.scheme);
        if (pair.accent === accent && pair.secondary === secondary)
            return name;
    }
    return "";
}

function clamp(value, minimum, maximum) {
    return Math.max(minimum, Math.min(maximum, Number(value)));
}

function channelHex(value) {
    var text = Math.round(value).toString(16);
    return text.length < 2 ? "0" + text : text;
}

function hsvToHex(hue, saturation, value) {
    var h = Number(hue);
    var s = Number(saturation);
    var v = Number(value);
    if (!isFinite(h) || !isFinite(s) || !isFinite(v))
        return null;
    h = ((h % 360) + 360) % 360;
    s = clamp(s, 0, 100) / 100;
    v = clamp(v, 0, 100) / 100;

    var chroma = v * s;
    var section = h / 60;
    var x = chroma * (1 - Math.abs(section % 2 - 1));
    var red = 0;
    var green = 0;
    var blue = 0;
    if (section < 1) { red = chroma; green = x; }
    else if (section < 2) { red = x; green = chroma; }
    else if (section < 3) { green = chroma; blue = x; }
    else if (section < 4) { green = x; blue = chroma; }
    else if (section < 5) { red = x; blue = chroma; }
    else { red = chroma; blue = x; }
    var match = v - chroma;
    return "#" + channelHex((red + match) * 255)
        + channelHex((green + match) * 255)
        + channelHex((blue + match) * 255);
}

function hexToHsv(value) {
    var color = normalizedColor(value);
    if (!color)
        return null;
    var red = parseInt(color.slice(1, 3), 16) / 255;
    var green = parseInt(color.slice(3, 5), 16) / 255;
    var blue = parseInt(color.slice(5, 7), 16) / 255;
    var maximum = Math.max(red, green, blue);
    var minimum = Math.min(red, green, blue);
    var delta = maximum - minimum;
    var hue = 0;
    if (delta !== 0) {
        if (maximum === red)
            hue = 60 * (((green - blue) / delta) % 6);
        else if (maximum === green)
            hue = 60 * ((blue - red) / delta + 2);
        else
            hue = 60 * ((red - green) / delta + 4);
    }
    if (hue < 0)
        hue += 360;
    return {
        h: Math.round(hue),
        s: Math.round((maximum === 0 ? 0 : delta / maximum) * 100),
        v: Math.round(maximum * 100)
    };
}

if (typeof module !== "undefined") {
    module.exports = {
        MAX_NAME_LENGTH: MAX_NAME_LENGTH,
        shippedProfiles: shippedProfiles,
        curatedAccents: curatedAccents,
        validCustomProfiles: validCustomProfiles,
        profileCatalog: profileCatalog,
        createCustomProfile: createCustomProfile,
        findProfile: findProfile,
        editProfile: editProfile,
        deleteProfile: deleteProfile,
        curatedPair: curatedPair,
        matchingShippedProfile: matchingShippedProfile,
        curatedNameForProfile: curatedNameForProfile,
        hsvToHex: hsvToHex,
        hexToHsv: hexToHsv
    };
}
