// Copyright (C) 2019-2021 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.

import {queryToParams, escapeHTML} from '/assets/js/common.js';
import {DEFAULT_ARCHITECTURE} from '/assets/js/constants.js'

// These are flipped delibrately, it makes the fromQuery function return configurations in an
// intuitive order.
const REQUIRED_MEMBERS = ['architecture', 'version', 'is_simulator', 'platform'];
const OPTIONAL_MEMBERS = ['flavor', 'style', 'model', 'version_name'];
const FILTERING_MEMBERS = ['sdk'];
const VERSION_OFFSET_CONSTANT = 1000;
const SDK_REGEX = /(\d+)([A-Z])(\d+)(.*)/;

// This class is very similar to the Python Configuration class
class Configuration {
    static members() {
        return [...FILTERING_MEMBERS, ...OPTIONAL_MEMBERS, ...REQUIRED_MEMBERS];
    }
    static fromQuery() {
        const params = queryToParams(document.URL.split('?')[1]);
        let configs = [new Configuration()];

        Configuration.members().forEach(member => {
            const argumentsForMember = params[member];
            if (!argumentsForMember || argumentsForMember.length === 0)
                return;

            let originalConfigs = configs;
            configs = [];
            argumentsForMember.forEach(argument => {
                originalConfigs.forEach(config => {
                    let newConfig = new Configuration(config);
                    newConfig[member] = argument;
                    configs.push(new Configuration(newConfig));
                });
            });
        });

        return configs;
    }
    static versionToInteger(version) {
        if (typeof version === 'number')
            return version;
        let versionList = null;
        if (typeof version === 'string')
            versionList = version.split('.');
        else {
            if (!Array.isArray(version))
                return null;
            versionList = version;
        }
        if (versionList.length < 1 || versionList.length > 3)
            return null;

        let result = 0;
        let index = 0;
        while (index < 3) {
            result *= VERSION_OFFSET_CONSTANT;
            if (index < versionList.length)
                result += parseInt(versionList[index]);
            ++index;
        }
        return result;
    }
    static integerToVersion(version) {
        let result = [0, 0, 0];
        for (let index = 0; index < result.length; ++index) {
            result[result.length - (index + 1)] = version % VERSION_OFFSET_CONSTANT;
            version = Math.floor(version / VERSION_OFFSET_CONSTANT);
        }
        return result.join('.');
    }

    static combine()
    {
        if (!arguments.length)
            return new Configuration();
        const combined = new Configuration(arguments[0]);
        for (let i = 1; i < arguments.length; ++i) {
            Configuration.members().forEach(member => {
                if (arguments[i][member] !== combined[member])
                    combined[member] = null;
            });
        }
        return combined;
    }

    static releaseForSDK(sdk)
    {
        if (!sdk)
            return '';
        const match = sdk.match(SDK_REGEX)
        if (!match)
            return '';
        if (['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'].includes(match[2]))
            return match[2];
        if (['J', 'R'].includes(match[2])) {
            const count = parseInt(match[3]);
            if (count < 500)
                return 'A';
            if (count < 700)
                return 'B';
            if (count < 800)
                return 'C';
            return 'D';
        }
        if (Number(match[1]) >= 23) {
            if (['M'].includes(match[2]))
                return 'A';
            if (['N'].includes(match[2])) {
                const count = parseInt(match[3]);
                if (count < 200)
                    return 'B';
                if (count < 500)
                    return 'C';
                return 'D'
            }
        } else {
            if (['N'].includes(match[2]) || ['M'].includes(match[2])) {
                const count = parseInt(match[3]);
                if (count < 500)
                    return 'A';
                if (count < 700)
                    return 'B';
                if (count <= 800 || count >= 840 && count < 870)
                    return 'C';
                return 'D';
            }
        }
        if (['K'].includes(match[2])) {
            const count = parseInt(match[3]);
            if (count < 200)
                return 'B';
            if (count < 510)
                return 'C';
            return 'D';
        }
        if (['S'].includes(match[2])) {
            return 'C';
        }
        if (['L', 'O', 'T'].includes(match[2])) {
            const count = parseInt(match[3]);
            if (count < 400)
                return 'E'
            return 'F';
        }
        if (['M', 'U'].includes(match[2]))
            return 'G';

        return '';
    }

    constructor(json = {}) {
        this.platform = json.platform ? json.platform : null;
        this.version = json.version ? Configuration.versionToInteger(json.version) : null;
        this.version_name = json.version_name ? json.version_name : null;
        this.sdk = json.sdk ? json.sdk : null;
        if (typeof json.is_simulator === 'string')
            this.is_simulator = json.is_simulator.toLowerCase() === 'true' ? true : false;
        else if (typeof json.is_simulator === 'boolean')
            this.is_simulator = json.is_simulator;
        else
            this.is_simulator = null;
        this.style = json.style ? json.style : null;
        this.flavor = json.flavor ? json.flavor : null;
        this.model = json.model ? json.model : null;
        this.architecture = json.architecture ? json.architecture : null;

        // It's useful to track versions from different major and minor SDK releases
        // in seperate timelines. Attempt to embed a release family in the displayed
        // version name computed from the SDK
        if (this.sdk && this.version_name && !(json instanceof Configuration)) {
            const release = Configuration.releaseForSDK(this.sdk)
            if (release) {
                const ending = this.version_name ? this.version_name.substring(this.version_name.length - 2) : null;
                if ([' A', ' B', ' C', ' D', ' E', ' F', ' G', ' H'].includes(ending))
                    this.version_name = this.version_name.substring(0, this.version_name.length - 2);
                this.version_name = `${this.version_name} ${release}`;
            }
        }
    }
    toKey() {
        let result = ''
        if (this.platform != null)
            result += ' ' + this.platform
        if (this.version_name != null)
            result += ' ' + this.version_name;
        if (this.version != null && (this.version_name == null || this.sdk != null))
            result += ' ' + Configuration.integerToVersion(this.version);
        if (this.sdk != null)
            result += ' (' + this.sdk + ')';
        if (this.is_simulator != null && this.is_simulator)
            result += ' Simulator';
        if (this.style != null)
            result += ' ' + this.style;
        if (this.flavor != null)
            result += ' ' + this.flavor;
        if (this.model != null)
            result += ' on ' + this.model;
        if (this.architecture != null)
            result += ' using ' + this.architecture;

        if (result)
            return result.substr(1);
        return 'All';
    }
    toString() {
        let result = '';

        // Version name implies platform and version
        if (this.version_name != null)
            result += ' ' + this.version_name;
        else {
            if (this.platform != null)
                result += ' ' + this.platform
            if (this.version != null)
                result += ' ' + Configuration.integerToVersion(this.version);
        }
        if (this.is_simulator != null && this.is_simulator)
            result += ' Simulator';
        if (this.flavor != null)
            result += ' ' + this.flavor;
        if (this.style != null) {
            result += ' ' + this.style.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('-');
        } if (this.model != null)
            result += ' on ' + this.model;
        if (this.architecture != null && (DEFAULT_ARCHITECTURE == null || this.architecture.search(DEFAULT_ARCHITECTURE) < 0))
            result += ' with ' + this.architecture;

        if (this.sdk != null)
            result += ' (' + this.sdk + ')';

        if (result)
            return escapeHTML(result.substr(1));
        return 'All';
    }
    compare(configuration) {
        if (configuration === null)
            return 1;

        const lst = [['platform'], ['is_simulator'], ['version_name', 'version'], ['flavor'], ['style'], ['architecture'], ['model']];
        let result = 0;
        lst.forEach(keyFamily => {
            // Grouping keys into families allows keys to be ignored if another is defined.
            // For example, if two configurations declare the same version name, despite having a different
            // version number, they will be treated as identical
            let wasKeyFamilyDefined = false;

            keyFamily.forEach(key => {
                if (result || wasKeyFamilyDefined)
                    return;

                if (this[key] !== null && configuration[key] !== null) {
                    if (typeof this[key] === 'string') {
                        if (key === 'version_name' && (this[key].startsWith(configuration[key]) || configuration[key].startsWith(this[key])))
                            result = 0;
                        else
                            result = this[key].localeCompare(configuration[key]);
                    }
                    else if (typeof this[key] === 'number' || typeof this[key] === 'boolean')
                        result = this[key] - configuration[key];
                    else
                        console.error(typeof this[key] + ' is not a recognized configuration type')
                    wasKeyFamilyDefined = true;
                }
            });
        });
        return result;
    }
    compareSDKs(configuration) {
        if (this.sdk === null || configuration.sdk === null)
            return 0;

        const thisMatch = this.sdk.match(SDK_REGEX);
        const configurationMatch = configuration.sdk.match(SDK_REGEX);

        if (!thisMatch && !configurationMatch) {
            console.error(`'${this.sdk}' and '${configuration.sdk}' do not match the SDK regular expression`);
            return this.sdk.localeCompare(configuration.sdk);
        }
        if (!thisMatch && configurationMatch)
            return -1;
        if (thisMatch && !configurationMatch)
            return 1;

        const majorDiff = parseInt(thisMatch[1]) - parseInt(configurationMatch[1]);
        if (majorDiff)
            return majorDiff;
        const minorDiff = thisMatch[2].localeCompare(configurationMatch[2]);
        if (minorDiff)
            return minorDiff;
        const buildDiff = parseInt(thisMatch[3]) - parseInt(configurationMatch[3]);
        if (buildDiff)
            return buildDiff;
        return thisMatch[4].localeCompare(configurationMatch[4]);
    }
    toParams() {
        let version_name = this.version_name;
        const ending = this.version_name ? this.version_name.substring(this.version_name.length - 2) : null;
        if ([' A', ' B', ' C', ' D', ' E', ' F', ' G', ' H'].includes(ending))
            version_name = this.version_name.substring(0, this.version_name.length - 2);
        let architecture = null;
        if (this.architecture != null && (DEFAULT_ARCHITECTURE == null || this.architecture.search(DEFAULT_ARCHITECTURE) < 0))
            architecture = this.architecture;
        return {
            platform: [this.platform],
            version:[this.version ? Configuration.integerToVersion(this.version) : null],
            version_name: [version_name],
            is_simulator: [this.is_simulator === null ? null : (this.is_simulator ? 'True' : 'False')],
            style: [this.style],
            flavor: [this.flavor],
            model: [this.model],
            architecture: [architecture],
        };
    }
}

export {Configuration};
