import * as fs from "fs";
import * as path from "path";
import {$log} from "ts-log-debug";
import Remote from "./remote";
import {Config, WorkspaceConfig} from "./config";
import {deserialize} from "class-transformer";
import {ensureDir, ensureDirSync, removeSync} from "fs-extra";
import {AssetFileStatus} from "./asset-file-status";
import {RepoError} from "./repo-error";
import {join, relative, resolve} from "path";
import {Version} from "./models/version";
import {AssetFile} from "./models/asset-file";

export default class Repo {

    static PACKAGE_NAME_RE = /(@?\w+\/?\w+\/?\w+)@?(.*)/gm;
    static CONFIG_NAME = 'modelnest.json';
    static WORKSPACE_CONFIG_NAME = 'nest-workspace.json';
    static WORKSPACE_DIRECTORY_NAME = 'nest_models';

    public config: Config;

    constructor(public root: string) {
        try {
            this.config = this.loadConfig();
        } catch (e) {
            this.config = new Config();
        }
    }

    init() {
        if (!fs.existsSync(this.dot)) {
            fs.mkdirSync(this.dot);
        }
        this.writeConfig(false);
    }

    get objectsPath(): string {
        return path.join(this.dot, 'objects');
    }

    get refsPath(): string {
        return path.join(this.dot, 'refs');
    }

    get dot(): string {
        return path.join(this.root, '.nest');
    }

    get exists(): boolean {
        return fs.existsSync(this.dot);
    }

    static findRepo(startDir?: string): Repo {
        let currentDir = startDir || process.cwd();
        while (currentDir != path.sep) {
            try {
                let repo = new Repo(currentDir);
                $log.debug(`Found repo ${currentDir}`);
                return repo;
            } catch {
                currentDir = path.dirname(currentDir);
            }
        }
        throw new Repo('Not a model nest repo');
    }

    loadConfig(): Config {
        const readpath = path.join(this.root, Repo.CONFIG_NAME);
        if (!fs.existsSync(readpath)) {
            throw new RepoError('Not a model nest repo')
        }
        const configString = fs.readFileSync(readpath, 'utf8');
        const config : Config = deserialize(Config, configString);
        return config;
    }

    writeConfig(overwrite: boolean = true) {
        this.jsonToRoot(this.config, Repo.CONFIG_NAME);
    }


    async add(workspace: string, packageVersion: AssetVersion) {
        $log.debug(`Adding asset "${packageVersion.name}' version "${packageVersion.version}" to "${workspace}"`);
        return this.downloadAsset(workspace, packageVersion);
    }

    async downloadAsset(workspace: string, packageVersion: AssetVersion): Promise<Version|null> {
        const remote = new Remote();
        const version = await remote.getAssetVersion(packageVersion);
        if (version) {
            const workspacePath = this.config.workspaces[workspace];
            const workspaceDirectory = path.join(this.root, workspacePath);
            const oldVersion = this.readAssetVersion(workspace, packageVersion.name);
            if (oldVersion) {
                this.deleteAssetVersionContents(workspaceDirectory, packageVersion, oldVersion);
            }

            remote.downloadAsset(workspaceDirectory, packageVersion, version);
            this.writeAssetVersion(workspace, packageVersion.name, version);
            const workspaceConfig = this.readWorkspace(workspace);
            workspaceConfig.assets[packageVersion.name] = version.version;
            this.writeWorkspaceConfig(workspace, workspaceConfig);
            return version;
        } else {
            return null;
        }
    }

    deleteAssetVersionContents(workspacePath: string, packageVersion: AssetVersion, version: Version) {
        for(const format of version.formats) {
            this.deleteAssetFile(workspacePath, packageVersion, format.root);
            for(const resource of format.resources) {
                this.deleteAssetFile(workspacePath, packageVersion, resource);
            }
        }
    }

    deleteAssetFile(workspacePath: string, packageVersion: AssetVersion, assetFile: AssetFile) {
        const filePath = path.join(workspacePath, packageVersion.name, assetFile.sourcePath);
        removeSync(filePath);
    }

    jsonToRoot(obj: any, fileName: string, overwrite: boolean = true) {
        const writePath = path.join(this.root, fileName);
        if (!overwrite && fs.existsSync(writePath)) {
            return;
        }

        ensureDir(path.dirname(writePath));

        const jsonData = JSON.stringify(obj, null, 2);
        fs.writeFile(writePath, jsonData, err => {
            if (err) $log.error(err)
        });
    }

    writeWorkspaceConfig(alias: string, obj: any, overwrite: boolean = true) {
        const workspacePath = this.config.workspaces[alias];
        const writePath = path.join(this.root, workspacePath, Repo.WORKSPACE_CONFIG_NAME);
        if (!overwrite && fs.existsSync(writePath)) {
            return;
        }

        ensureDirSync(path.dirname(writePath));

        const jsonData = JSON.stringify(obj, null, 2);
        fs.writeFile(writePath, jsonData, err => {
            if (err) $log.error(err)
        });
    }

    readAssetVersion(alias: string, assetName:string): Version | null {
        const workspacePath = this.config.workspaces[alias];
        const readPath = join(this.root, workspacePath, assetName, 'version.json');
        if (fs.existsSync(readPath)) {
            const configString = fs.readFileSync(readPath, 'utf8');
            const assetFileRef : Version = JSON.parse(configString);
            return assetFileRef;
        }
        return null;
    }

    writeAssetVersion(alias: string, assetName:string, obj: any, overwrite: boolean = true) {
        const workspacePath = this.config.workspaces[alias];
        const writePath = join(this.root, workspacePath, assetName, 'version.json');
        if (!overwrite && fs.existsSync(writePath)) {
            return;
        }

        ensureDirSync(path.dirname(writePath));

        const jsonData = JSON.stringify(obj, null, 2);
        fs.writeFile(writePath, jsonData, err => {
            if (err) $log.error(err)
        });
    }

    async status(): Promise<Array<AssetFileStatus>> {
        const remote = new Remote();

        var assetStatuses : Array<AssetFileStatus> = [];
        for(const workspace of Object.keys(this.config.workspaces)) {
            const workspaceConfig = this.readWorkspace(workspace);
            for(const asset of Object.keys(workspaceConfig.assets)) {
                const currentVersion = workspaceConfig.assets[asset];
                const latestVersion = await remote.getAssetVersion({version: 'latest', name: asset});
                if (latestVersion) {
                    assetStatuses.push(new AssetFileStatus(
                        workspace,
                        {
                            name: asset,
                            version: currentVersion,
                        }, {
                            name: asset,
                            version: latestVersion.version,
                        })
                    );
                }
            }
        }
        return assetStatuses;
    }

    async update(workspace?:string, asset?: string): Promise<Array<AssetFileStatus>> {
        $log.debug(`Updated ${asset}`);
        var newVersions : Array<AssetFileStatus> = [];

        if (workspace && asset) {
            const version = await this.downloadAsset(workspace, {
                name: asset,
                version: 'latest',
            });
            if (version) newVersions.push(new AssetFileStatus(
                workspace,
                {
                    name: asset,
                    version: version.version,
                })
            );
        } else {
            for(const workspace of Object.keys(this.config.workspaces)) {
                const workspaceConfig = this.readWorkspace(workspace);
                for(const asset of Object.keys(workspaceConfig.assets)) {
                    const version = await this.downloadAsset(workspace, {
                        name: asset,
                        version: 'latest',
                    });
                    if (version) newVersions.push(new AssetFileStatus(
                        workspace,
                        {
                            name: asset,
                            version: version.version,
                        })
                    );
                }
            }
        }
        return newVersions;
    }

    readWorkspace(workspace: string): WorkspaceConfig {
        const path = this.config.workspaces[workspace];
        const readpath = join(join(this.root, path), Repo.WORKSPACE_CONFIG_NAME);
        if (!fs.existsSync(readpath)) {
            throw new RepoError('Not a model nest repo')
        }
        const configString = fs.readFileSync(readpath, 'utf8');
        const config : WorkspaceConfig = deserialize(WorkspaceConfig, configString);
        return config;
    }

    createWorkspace(alias: string, path: string) {
        const abspath = resolve(join(path, Repo.WORKSPACE_DIRECTORY_NAME));
        const projRelative = relative(this.root, abspath);
        this.config.workspaces[alias] = projRelative;
        ensureDir(abspath);
        this.writeWorkspaceConfig(alias, new WorkspaceConfig());
        this.writeConfig();
    }

    static splitPackageVersion(modelId: string): AssetVersion {
        let m;
        let packageName: string = '';
        let version: string = 'latest';
        let parts = modelId.split('@');
        if (parts.length > 0) {
            packageName = parts[0];
        }
        if (parts.length == 2) {
            version = parts[1];
        }

        return {name: packageName, version}
    }
}