import {AppLocal} from './app-local';
import * as path from 'path';
import * as ini from 'ini';
import * as fs from 'fs';
import * as child_process from 'child_process';
import * as Mustache from 'mustache';

export enum Category {
    game,
    music,
    book,
    runtime,
    emulator,
    language,
    expansion,
    module
}
// export type CategoryString = 'game' | 'music' | 'book' | 'runtime' | 'emulator' | 'language' | 'expansion' | 'module'

// export enum DownloadStatus{
//     downloading,
//     init,
//     installing,
//     ready,
//     updating,
//     uninstalling,
//     waiting,
// }
export interface BaseAction {
    execute: string;
    args: string[];
    env: {};
}

export interface Action extends BaseAction {
    interpreter?: string;
    open?: App;
}

export interface SpawnAction extends BaseAction {
    cwd?: string;
}

export class FileOptions {
    sync: boolean;
    ignore: boolean;
}

export class AppStatus {
    progress: number;
    total: number;
    private _status: string;
    get status(): string {
        return this._status;
    }

    set status(status: string) {
        this.progress = 0;
        this.total = 0;
        this.progressMessage = '';
        this._status = status;
    }

    progressMessage: string;
}

export interface YGOProDistroData {
    deckPath: string;
    replayPath: string;
    systemConf: string;
}

export class App {
    id: string;
    name: string;          // i18n

    description: string;   // i18n
    author: string;             // English Only
    homepage: string;
    developers: { name: string, url: string }[];
    released_at: Date;
    updated_at: Date;
    category: Category;
    parent?: App;

    actions: Map<string, Action>;
    references: Map<string, App>;
    dependencies: Map<string, App>;
    locales: string[];
    news: { title: string, url: string, image: string, updated_at: Date }[];
    network: any;
    tags: string[];
    version: string;
    local: AppLocal | null;
    status: AppStatus;
    conference: string | undefined;
    files: Map<string, FileOptions>;
    data: any;

    icon: string;
    cover: string;
    background: string;

    price: { [currency: string]: string };
    key?: string

    static getQuerySuffix(platform: string, locale: string, arch: string) {
        const params = new URLSearchParams();
        params.set('platform', platform);
        params.set('locale', locale);
        params.set('arch', arch);
        return params.toString();
    }

    static downloadUrl(app: App, platform: string, locale: string, arch: string): string {
        /*
        if (app.id === 'ygopro') {
            return `https://sthief.moecube.com:444/metalinks/${app.id}-${process.platform}-${locale}/${app.version}`;
        } else if (app.id === 'desmume') {
            return `https://sthief.moecube.com:444/metalinks/${app.id}-${process.platform}/${app.version}`;
        }
        return `https://sthief.moecube.com:444/metalinks/${app.id}/${app.version}`;

         */
        return `https://sapi.moecube.com:444/release/update/metalinks/${app.id}/${app.version}?${this.getQuerySuffix(platform, locale, arch)}`;
    }


    static checksumUrl(app: App, platform: string, locale: string, arch: string): string {
        /*if (app.id === 'ygopro') {
            return `https://sthief.moecube.com:444/checksums/${app.id}-${platform}-${locale}/${app.version}`;
        } else if (app.id === 'desmume') {
            return `https://sthief.moecube.com:444/checksums/${app.id}-${platform}/${app.version}`;
        }
        return `https://sthief.moecube.com:444/checksums/${app.id}/${app.version}`;*/
        return `https://sapi.moecube.com:444/release/update/checksums/${app.id}/${app.version}?${this.getQuerySuffix(platform, locale, arch)}`;
    }

    static updateUrl(app: App, platform: string, locale: string, arch: string): string {
        /*if (app.id === 'ygopro') {
            return `https://sthief.moecube.com:444/update/${app.id}-${platform}-${locale}/${app.version}`;
        } else if (app.id === 'desmume') {
            return `https://sthief.moecube.com:444/update/${app.id}-${platform}/${app.version}`;
        }*/
        return `https://sapi.moecube.com:444/release/update/update/${app.id}/${app.version}?${this.getQuerySuffix(platform, locale, arch)}`;
    }

    isBought(): Boolean {
        // 免费或有 Key
        return !this.price || !!this.key;
    }

    isLanguage() {
        return this.category === Category.module && this.tags.includes('language');
    }

    reset() {
        this.status.status = 'init';
        this.local = null;
        localStorage.removeItem(this.id);
    }

    isInstalled(): boolean {
        return this.status.status !== 'init';
    }

    isReady(): boolean {
        return this.status.status === 'ready';
    }

    isInstalling(): boolean {
        return this.status.status === 'installing';
    }

    isWaiting(): boolean {
        return this.status.status === 'waiting';
    }

    isDownloading(): boolean {
        return this.status.status === 'downloading';
    }

    isUninstalling(): boolean {
        return this.status.status === 'uninstalling';
    }

    isUpdating(): boolean {
        return this.status.status === 'updating';
    }

    isGettingFileList(): boolean {
        return this.status.status === 'gettingFileList';
    }

    isVerifying(): boolean {
        return this.status.status === 'verifying';
    }

    runnable(): boolean {
        return [Category.game].includes(this.category);
    }

    progressMessage(): string | undefined {
        return this.status.progressMessage;
    }

    constructor(app: any) {
        this.id = app.id;
        this.name = app.name;
        this.description = app.description;
        this.developers = app.developers;
        this.released_at = app.released_at;
        this.updated_at = app.updated_at;
        this.author = app.author;
        this.homepage = app.homepage;
        this.category = Category[<string>app.category];
        this.actions = app.actions;
        this.dependencies = app.dependencies;
        this.parent = app.parent;
        this.references = app.references;
        this.locales = app.locales;
        this.news = app.news;
        this.network = app.network;
        this.tags = app.tags;
        this.version = app.version;
        this.conference = app.conference;
        this.files = app.files;
        this.data = app.data;

        this.icon = app.icon;
        this.cover = app.cover;
        this.background = app.background;

        this.price = app.price;
        this.key = app.key;
    }

    findDependencies(): App[] {
        if (this.dependencies && this.dependencies.size > 0) {
            let set = new Set<App>();
            for (let dependency of this.dependencies.values()) {
                dependency.findDependencies()
                    .forEach((value) => {
                        set.add(value);
                    });
                set.add(dependency);
            }
            return Array.from(set);
        }
        return [];
    }

    readyForInstall(): boolean {
        let dependencies = this.findDependencies();
        return dependencies.every((dependency) => dependency.isReady());
    }

    async getSpawnAction(children: App[], action_name = 'main', referencedApp?: App, referencedAction?: Action, cwd?: string, argsTemplate?: any): Promise<SpawnAction> {
        const appCwd = (<AppLocal>this.local).path;
        if(!cwd) {
            cwd = appCwd;
        }
        if (this.id === 'np2fmgen') {
            const config_file = path.join(this.local!.path, 'np21nt.ini');
            let config = await new Promise((resolve, reject) => {
                fs.readFile(config_file, {encoding: 'utf-8'}, (error, data) => {
                    if (error) {
                        return reject(error);
                    }
                    resolve(ini.parse(data));
                });
            });
            const default_config = {
                clk_mult: '48',
                DIPswtch: '3e f3 7b',
                SampleHz: '44100',
                Latencys: '100',
                MIX_TYPE: 'true',
                windtype: '0'
            };
            config['NekoProject21'] = Object.assign({}, default_config, config['NekoProject21']);
            config['NekoProject21']['HDD1FILE'] =
                path.win32.join(process.platform === 'win32' ? '' : 'Z:', referencedApp.local!.path, referencedAction.execute);
            config['NekoProject21']['fontfile'] =
                path.win32.join(process.platform === 'win32' ? '' : 'Z:', referencedApp.local!.path, 'font.bmp');
            await new Promise((resolve, reject) => {
                fs.writeFile(config_file, ini.stringify(config), (error) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve(null);
                    }
                });
            });
            cwd = appCwd;
        }

        let action: Action = <Action>this.actions.get(action_name);
        let args: string[] = [];
        let env = Object.assign({}, process.env);
        for (let child of children) {
            if (child.isInstalled()) {
                let _action = child.actions.get(action_name);
                if (_action) {
                    action = _action;
                }
            }
        }
        let execute: string;
        const appExecute = path.join(appCwd, action.execute);
        if (action.interpreter) {
            execute = action.interpreter;
            args.push(appExecute);
        } else {
            execute = appExecute;
        }

        if (action.open) {
            const np2 = action.open;
            const openAction = await np2.getSpawnAction([], 'main', this, action, cwd, argsTemplate);
            args = args.concat(openAction.args);
            args.push(action.execute);
            execute = openAction.execute;
            cwd = openAction.cwd;
        }
        args = args.concat(action.args);
        if (argsTemplate) {
            for (let i = 0; i < args.length; ++i) {
                if (typeof args[i] !== 'string') {
                    continue;
                }
                args[i] = Mustache.render(args[i], argsTemplate, null, { escape: (v) => v });
            }
        }
        env = Object.assign(env, action.env);
        return {
            execute,
            args,
            env,
            cwd
        };
    }

    async spawnApp(children: App[], action_name = 'main', argsTemplate?: any) {

        if (this.id === 'th123') {
            let th105 = <App>this.references.get('th105');
            if (th105.isInstalled()) {
                console.log(`Reference of th123: ${th105}`);
                const config_file = path.join((<AppLocal>this.local).path, 'configex123.ini');
                let config = ini.parse(await fs.promises.readFile(config_file, { encoding: 'utf-8' }));
                const th105LocalApp = (<AppLocal>th105.local);
                const targetTh105Path = th105LocalApp ? th105LocalApp.path : (<AppLocal>this.local).path.replace(/th123/g, 'th105');
                config['th105path'] = {path: targetTh105Path};
                await fs.promises.writeFile(config_file, ini.stringify(config));
            }
        }

        const appCwd = (<AppLocal>this.local).path;
        const {execute, args, env, cwd} = await this.getSpawnAction(children, action_name, null, null, null, argsTemplate);
        console.log(execute, args, env, cwd, appCwd);
        return child_process.spawn(execute, args, {env: env, cwd: cwd || appCwd});
    }

    get isYGOPro(): boolean {
        return !!this.ygoproDistroData;
    }

    get ygoproDistroData(): YGOProDistroData {
        if(!this.data) {
            return null;
        }
        return this.data.ygopro || null;
    }

    get ygoproDeckPath(): string {
        const distroData = this.ygoproDistroData;
        if(!distroData) {
            return null;
        }
        return path.join(this.local!.path, distroData.deckPath);
    }

    get ygoproReplayPath(): string {
        const distroData = this.ygoproDistroData;
        if(!distroData || !distroData.replayPath) {
            return null;
        }
        return path.join(this.local!.path, distroData.replayPath);
    }

    get systemConfPath(): string {
        const distroData = this.ygoproDistroData;
        if(!distroData || !distroData.systemConf) {
            return null;
        }
        return path.join(this.local!.path, distroData.systemConf);
    }

}
