let File = require('../File');
let VueVersion = require('../VueVersion');
let AppendVueStylesPlugin = require('../webpackPlugins/Css/AppendVueStylesPlugin');

/** @typedef {import("vue").VueLoaderOptions} VueLoaderOptions */

class Vue {
    /**
     * Register the component.
     *
     * @param {object} options
     * @param {number} [options.version] Which version of Vue to support. Detected automatically if not given.
     * @param {string|null} [options.globalStyles] A file to include w/ every vue style block.
     * @param {boolean|string} [options.extractStyles] Whether or not to extract vue styles. If given a string the name of the file to extract to.
     * @param {boolean} [options.useVueStyleLoader] Use vue-style-loader to extract Vue Styles.
     * @param {VueLoaderOptions} [options.options] Options to pass to Vue Loader
     */
    register(options = {}) {
        if (
            arguments.length === 2 &&
            typeof arguments[0] === 'string' &&
            typeof arguments[1] === 'string'
        ) {
            throw new Error(
                'mix.vue() is a feature flag. Use mix.js(source, destination).vue() instead'
            );
        }

        this.version = new VueVersion(this.context).detect(options.version);

        this.options = Object.assign(
            {
                options: null,
                globalStyles: null,
                extractStyles: false,
                useVueStyleLoader: false
            },
            options
        );

        this.context.globalStyles = this.options.globalStyles;
        this.context.extractingStyles =
            this.context.extractingStyles || !!this.options.extractStyles;

        this.addDefines();
    }

    /**
     * Required dependencies for the component.
     */
    dependencies() {
        this.requiresReload = true;

        let dependencies = [
            this.version === 2 ? 'vue-template-compiler' : '@vue/compiler-sfc',
            this.version === 2 ? 'vue-loader@^15.9.7' : 'vue-loader@^16.2.0'
        ];

        if (this.options.extractStyles && this.options.globalStyles) {
            dependencies.push('sass-resources-loader');
        }

        return dependencies;
    }

    /**
     * Override the generated webpack configuration.
     *
     * @param {Object} webpackConfig
     */
    webpackConfig(webpackConfig) {
        // push -> unshift to combat vue loader webpack 5 bug
        webpackConfig.module.rules.unshift({
            test: /\.vue$/,
            use: [
                {
                    loader: this.context.resolve('vue-loader'),
                    options: this.options.options || Config.vue || {}
                }
            ]
        });

        // Alias Vue to its ESM build if the user has not already given an alias
        webpackConfig.resolve.alias = webpackConfig.resolve.alias || {};

        if (!webpackConfig.resolve.alias['vue$']) {
            webpackConfig.resolve.alias['vue$'] = this.aliasPath();
        }

        webpackConfig.resolve.extensions.push('.vue');

        this.updateChunks();
    }

    aliasPath() {
        if (this.version === 2) {
            return this.options.runtimeOnly
                ? 'vue/dist/vue.runtime.esm.js'
                : 'vue/dist/vue.esm.js';
        }

        return this.options.runtimeOnly
            ? 'vue/dist/vue.runtime.esm-bundler.js'
            : 'vue/dist/vue.esm-bundler.js';
    }

    /**
     * webpack plugins to be appended to the master config.
     */
    webpackPlugins() {
        let { VueLoaderPlugin } = require(this.context.resolve('vue-loader'));

        return [new VueLoaderPlugin(), new AppendVueStylesPlugin()];
    }

    /**
     * Update CSS chunks to extract vue styles
     */
    updateChunks() {
        if (this.options.extractStyles === false) {
            return;
        }

        this.context.chunks.add(
            'styles-vue',
            this.styleChunkName(),
            [/.vue$/, module => module.type === 'css/mini-extract'],
            {
                chunks: 'all',
                enforce: true,
                type: 'css/mini-extract'
            }
        );

        this.context.chunks.add(
            'styles-jsx',
            this.styleChunkName(),
            [/.jsx$/, module => module.type === 'css/mini-extract'],
            {
                chunks: 'all',
                enforce: true,
                type: 'css/mini-extract'
            }
        );
    }

    /**
     * Get the name of the style chunk.
     *
     * @returns {string}
     */
    styleChunkName() {
        // If the user set extractStyles: true, we'll try
        // to append the Vue styles to an existing CSS chunk.
        if (this.options.extractStyles === true) {
            let chunk = this.context.chunks.find((chunk, id) => id.startsWith('styles-'));

            if (chunk) {
                return chunk.name;
            }
        }

        return this.extractFile().relativePathWithoutExtension();
    }

    /**
     * Get a new File instance for the extracted file.
     *
     * @returns {File}
     */
    extractFile() {
        return new File(this.extractFileName());
    }

    /**
     * Determine the extract file name.
     *
     * @return {string}
     */
    extractFileName() {
        let fileName =
            typeof this.options.extractStyles === 'string'
                ? this.options.extractStyles
                : '/css/vue-styles.css';

        return fileName.replace(Config.publicPath, '').replace(/^\//, '');
    }

    /**
     * Determine the extract file name.
     *
     * @internal
     */
    addDefines() {
        if (this.version === 2) {
            return;
        }

        this.context.api.define({
            __VUE_OPTIONS_API__: 'true',
            __VUE_PROD_DEVTOOLS__: 'false'
        });
    }

    /**
     * @internal
     **/
    get context() {
        return global.Mix;
    }
}

module.exports = Vue;
