#include commitment.js
#include xhr.js

const Flashii = function(baseUrl) {
    if(typeof baseUrl !== 'string')
        throw new Error('baseUrl must be a string');
    if(baseUrl.indexOf('//') === 0)
        baseUrl = window.location.protocol + baseUrl;

    const sortArray = typeof Array.prototype.toSorted === 'function'
        ? array => array.toSorted()
        : array => {
            array = array.slice();
            array.sort();
            return array;
        };

    const convertFields = fields => {
        if(fields === true)
            return '*';

        if(Array.isArray(fields))
            return sortArray(fields).join(',');

        if(typeof fields === 'string')
            return fields;

        return null;
    };

    const createUrl = (path, fields=null) => {
        const url = baseUrl + path;
        const params = {};

        if(fields)
            params['fields'] = convertFields(fields);

        return {
            setParam(name, value) {
                params[name] = value;
            },
            deleteParam(name) {
                delete params[name];
            },
            toString() {
                let str = url;

                const queryParts = [];
                for(const name in params)
                    if(params.hasOwnProperty(name))
                        queryParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(params[name]));
                if(queryParts.length > 0)
                    str += (str.indexOf('?') < 0 ? '?' : '&') + queryParts.join('&');

                return str;
            },
        };
    };

    const send = ({
        method,
        path,
        fresh=false,
        params=null,
        fields=null,
        body=null,
        headers=null,
        type='json',
        authed=false,
    }) => {
        return new Commitment((success, fail) => {
            const url = createUrl(path, fields);
            if(params)
                for(const name in params) {
                    if(name === 'fields' || params[name] === null || params[name] === undefined)
                        continue;

                    url.setParam(name, params[name]);
                }

            headers ??= {};
            if(fresh) headers['Cache-Control'] = 'no-cache';

            const options = { type, headers, authed };

            $xhr.send(method, url, options, body)
                .success(success)
                .fail(fail)
                .run();
        });
    };

    const fii = {};

    fii.v1 = {};

    const verifyChatServerId = id => {
        if(typeof id === 'number')
            id = id.toString();
        if(/^([^0-9]+)$/gu.test(id))
            throw new Error('id argument is not an acceptable chat server id.');
        return id;
    };

    fii.v1.chat = {};
    fii.v1.chat.login = function({ redirect=null, assign=true }) {
        return new Commitment(success => {
            redirect ??= `${location.protocol}//${location.host}`;
            if(typeof redirect !== 'string')
                throw new Error('redirect must a string.');

            const url = createUrl('/v1/chat/login');
            url.setParam('redirect', redirect);

            // intentionally does not call success
            if(assign)
                location.assign(url);
            else
                success(url);
        });
    };
    fii.v1.chat.servers = function({ proto=null, secure=null, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            const params = {};

            if(proto !== null && typeof proto !== 'string')
                throw new Error('proto must be a string or null');
            params['proto'] = proto;

            if(secure !== null)
                params['secure'] = secure ? '1' : '0';

            send({ method: 'GET', path: '/v1/chat/servers', fields, fresh, params })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('An argument contains an unsupported value.');
                    if(status > 299)
                        throw new Error(`Failed to fetch chat server list with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.chat.servers.server = function({ id, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            id = verifyChatServerId(id);

            send({ method: 'GET', path: `/v1/chat/servers/${id}`, fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status === 404)
                        throw new Error('No chat server with that id could be found.');
                    if(status > 299)
                        throw new Error(`Failed to fetch chat server with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.chat.servers.recommended = function({ proto, secure=null, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            if(typeof proto !== 'string')
                throw new Error('proto must be a string');

            const params = { proto };
            if(secure !== null)
                params['secure'] = secure ? '1' : '0';

            send({ method: 'GET', path: '/v1/chat/servers/recommended', fields, fresh, params })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('An argument contains an unsupported value.');
                    if(status === 404)
                        throw new Error('Was unable to determine a server based on the requested parameters.');
                    if(status > 299)
                        throw new Error(`Failed to fetch recommended chat server with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.chat.token = function() {
        return new Commitment((success, fail) => {
            send({ method: 'GET', path: '/v1/chat/token', authed: true, fresh: true })
                .success(({ status, body }) => {
                    if(status === 403)
                        throw new Error('You must be logged in to use chat.');
                    if(status > 299)
                        throw new Error(`Failed to fetch authorization token with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };

    const verifyColourPresetName = name => {
        if(/^([^A-Za-z0-9\-_]+)$/gu.test(name))
            throw new Error('name argument is not an acceptable colour preset name.');
        return name;
    };

    fii.v1.colours = {};
    fii.v1.colours.presets = function({ fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            send({ method: 'GET', path: '/v1/colours/presets', fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status > 299)
                        throw new Error(`Failed to fetch colour presets with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.colours.presets.preset = function({ name, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            name = verifyColourPresetName(name);

            send({ method: 'GET', path: `/v1/colours/presets/${name}`, fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status === 404)
                        throw new Error('Requested colour preset does not exist.');
                    if(status > 299)
                        throw new Error(`Failed to fetch colour preset "${name}" with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };

    const verifyEmoticonId = id => {
        if(typeof id === 'number')
            id = id.toString();
        if(/^([^0-9]+)$/gu.test(id))
            throw new Error('id argument is not an acceptable emoticon id.');
        return id;
    };

    fii.v1.emotes = function({ fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            send({ method: 'GET', path: '/v1/emotes', fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status > 299)
                        throw new Error(`Failed to fetch emoticons with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.emotes.emote = function({ id, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            id = verifyEmoticonId(id);

            send({ method: 'GET', path: `/v1/emotes/${id}`, fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status === 404)
                        throw new Error('Requested emoticon does not exist.');
                    if(status > 299)
                        throw new Error(`Failed to fetch emoticon "${id}" with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.emotes.emote.strings = function({ id, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            id = verifyEmoticonId(id);

            send({ method: 'GET', path: `/v1/emotes/${id}/strings`, fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status === 404)
                        throw new Error('Requested emoticon does not exist.');
                    if(status > 299)
                        throw new Error(`Failed to fetch emoticon "${id}" with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };

    const verifyKaomojiId = id => {
        if(typeof id === 'number')
            id = id.toString();
        if(/^([^0-9]+)$/gu.test(id))
            throw new Error('id argument is not an acceptable kaomoji id.');
        return id;
    };

    fii.v1.kaomoji = function({ as=null, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            send({ method: 'GET', path: '/v1/kaomoji', params: { as }, fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('As or fields argument contains unsupported value.');
                    if(status > 299)
                        throw new Error(`Failed to fetch kaomoji with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };
    fii.v1.kaomoji.kaomoji = function({ id, fields=null, fresh=false }) {
        return new Commitment((success, fail) => {
            id = verifyKaomojiId(id);

            send({ method: 'GET', path: `/v1/kaomoji/${id}`, fields, fresh })
                .success(({ status, body }) => {
                    if(status === 400)
                        throw new Error('Fields argument contains unsupported value.');
                    if(status === 404)
                        throw new Error('Requested kaomoji does not exist.');
                    if(status > 299)
                        throw new Error(`Failed to fetch kaomoji "${id}" with error code ${status}.`);

                    success(body);
                })
                .fail(fail)
                .run();
        });
    };

    return fii;
};