import * as fs from "fs";
import * as path from "path";
import {$log} from "ts-log-debug";
import Remote from "./remote";
import {ModelNestConfig} 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";
import {ExpectedError} from "clime";

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 MODEL_NEST_DIRECTORY = 'nest_models';

    public config: ModelNestConfig;

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

    init(modelNestDirectory?: string) {
        if (modelNestDirectory) {
            this.config.nest = modelNestDirectory;
        }
        this.writeConfig(false);
        ensureDirSync(this.modelNestDirectory);
    }

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

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

    get package(): string {
        return path.join(this.root, Repo.CONFIG_NAME);
    }

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

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

    loadConfig(): ModelNestConfig {
        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 : ModelNestConfig = deserialize(ModelNestConfig, configString);
        return config;
    }

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


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

    async downloadAsset(packageVersion: AssetVersion): Promise<Version|null> {
        const remote = new Remote();
        const version = await remote.getAssetVersion(packageVersion);
        if (version) {
            const oldVersion = this.readAssetVersion(packageVersion.name);
            if (oldVersion) {
                this.deleteAssetVersionContents(packageVersion, oldVersion);
            }

            await remote.downloadAsset(this.modelNestDirectory, packageVersion, version);
            this.writeAssetVersion(packageVersion.name, version);
            this.loadConfig();
            const packageConfig = this.config;
            packageConfig.assets[packageVersion.name] = version.version;
            this.writeConfig();
            return version;
        } else {
            return null;
        }
    }

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

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

    writeToRoot(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.writeFileSync(writePath, jsonData);
    }


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

    writeAssetVersion(assetName:string, obj: any, overwrite: boolean = true) {
        const writePath = join(this.modelNestDirectory, 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> = [];
        const workspaceConfig = this.config;
        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(
                    {
                        name: asset,
                        version: currentVersion,
                    }, {
                        name: asset,
                        version: latestVersion.version,
                    })
                );
            }
        }
        return assetStatuses;
    }

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

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

    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}
    }
}