import * as fs from 'fs-extra';
import * as path from 'path';
import * as babel from '@babel/core';
import * as globby from 'globby';

import { kebab2Camel, Camel2kebab, normalizeName } from '../utils';
import { VueFile, Library, VueFileExtendMode, JSFile } from '.';

export class FileExistsError extends Error {
    constructor(fullPath: string) {
        super(fullPath);
        this.name = 'FileExistsError';
    }
}

export function handleSame(dirPath: string, baseName: string = 'u-sample') {
    let dest = path.resolve(dirPath, `${baseName}.vue`);

    if (fs.existsSync(dest))
        throw new FileExistsError(dest);

    return dest;
}

export type Replacer = [RegExp, string];
export async function batchReplace(src: string | Array<string>, replacers: Array<Replacer>) {
    if (typeof src === 'string')
        src = [src];
    return Promise.all(src.map((fullPath) =>
        fs.readFile(fullPath, 'utf8').then((content) => {
            replacers.forEach((replacer) => content = content.replace(...replacer));
            return fs.writeFile(fullPath, content);
        })
    ));
}

export interface ListFilesFilters {
    type?: string, // both, file, directory
    all?: boolean,
    patterns?: Array<string>,
    includes?: string | RegExp | Array<string | RegExp>,
    excludes?: string | RegExp | Array<string | RegExp>,
    filters?: ((fullPath: string) => boolean) | Array<(fullPath: string) => boolean>,
};

export function listFiles(dir?: string, filters: ListFilesFilters = {}, recursive: boolean = false) {
    const pattern = recursive ? '**' : '*';
    return globby.sync([dir ? dir + path.sep + pattern : pattern].concat(filters.patterns || []), {
        dot: filters.all,
        onlyFiles: false,
    }).filter((filePath) => {
        if (filters.type) {
            const stat = fs.statSync(filePath);
            if (filters.type === 'file' && !stat.isFile())
                return false;
            if (filters.type === 'directory' && !stat.isDirectory())
                return false;
            if (filters.type === 'link' && !stat.isSymbolicLink())
                return false;
        }
        if (filters.includes) {
            if (!Array.isArray(filters.includes))
                filters.includes = [filters.includes];
            if (!filters.includes.every((include) => {
                if (typeof include === 'string')
                    return filePath.includes(include);
                else
                    return include.test(filePath);
            })) return false;
        }
        if (filters.excludes) {
            if (!Array.isArray(filters.excludes))
                filters.excludes = [filters.excludes];
            if (filters.excludes.some((exclude) => {
                if (typeof exclude === 'string')
                    return filePath.includes(exclude);
                else
                    return exclude.test(filePath);
            })) return false;
        }
        if (filters.filters) {
            if (!Array.isArray(filters.filters))
                filters.filters = [filters.filters];
            if (!filters.filters.every((filter) => filter(filePath)))
                return false;
        }
        return true;
    });
}

export function listAllFiles(dir?: string, filters: ListFilesFilters = {}) {
    return listFiles(dir, filters, true);
}

/* 以下代码复制粘贴写得冗余了一点，不过之后可能各部分功能会有差异，所以先不整合 */

export async function createDirectory(dirPath: string, dirName: string) {
    const dest = path.resolve(dirPath, dirName);
    if (fs.existsSync(dest))
        throw new FileExistsError(dest);

    await fs.mkdir(dest);
    return dest;
}

export async function moveFileToTrash(fullPath: string) {
    // @TODO: Windows, Linux
    const fileName = path.basename(fullPath);
    let dest = path.resolve(process.env.HOME, '.Trash', fileName);
    if (fs.existsSync(dest)) {
        const date = new Date();
        dest = dest.replace(/(\.[a-zA-Z]+$|$)/, `.${date.toTimeString().split(' ')[0].replace(/:/g, '-')}-${date.getMilliseconds()}$1`);
    }
    await fs.move(fullPath, dest);
    return dest;
}

export async function deleteFile(fullPath: string) {
    // @TODO: Windows, Linux
    await fs.remove(fullPath);
}

export async function rename(fullPath: string, newName: string) {
    const dest = path.join(path.dirname(fullPath), newName);
    if (dest === fullPath)
        return dest;

    if (fs.existsSync(dest))
        throw new FileExistsError(dest);

    await fs.move(fullPath, dest);
    return dest;
}

export async function createSingleFile(dirPath: string, componentName?: string) {
    const normalized = normalizeName(componentName);
    const dest = handleSame(dirPath, normalized.baseName);
    await fs.copy(path.resolve(__dirname, '../../templates/u-single-file.vue'), dest);

    if (normalized.baseName !== 'u-sample') {
        await batchReplace(dest, [
            [/u-sample/g, normalized.baseName],
            [/USample/g, normalized.componentName],
        ]);
    }
    return dest;
}

export async function createMultiFile(dirPath: string, componentName?: string) {
    const normalized = normalizeName(componentName);
    const dest = handleSame(dirPath, normalized.baseName);
    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file.vue'), dest);

    if (normalized.baseName !== 'u-sample') {
        await batchReplace([
            path.join(dest, 'index.js'),
            path.join(dest, 'README.md'),
        ], [
            [/u-sample/g, normalized.baseName],
            [/USample/g, normalized.componentName],
        ]);
    }
    return dest;
}

export async function createMultiFileWithSubdocs(dirPath: string, componentName?: string) {
    const normalized = normalizeName(componentName);
    const dest = handleSame(dirPath, normalized.baseName);
    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-subdocs.vue'), dest);

    if (normalized.baseName !== 'u-sample') {
        await batchReplace([
            path.join(dest, 'index.js'),
            path.join(dest, 'docs/api.md'),
            path.join(dest, 'docs/examples.md'),
        ], [
            [/u-sample/g, normalized.baseName],
            [/USample/g, normalized.componentName],
        ]);
    }
    return dest;
}

export async function createMultiFileWithScreenshots(dirPath: string, componentName?: string) {
    const normalized = normalizeName(componentName);
    const dest = handleSame(dirPath, normalized.baseName);
    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-screenshots.vue'), dest);

    if (normalized.baseName !== 'u-sample') {
        await batchReplace([
            path.join(dest, 'index.js'),
            path.join(dest, 'README.md'),
        ], [
            [/u-sample/g, normalized.baseName],
            [/USample/g, normalized.componentName],
        ]);
    }
    return dest;
}

export async function createMultiFilePackage(dirPath: string, componentName?: string) {
    const normalized = normalizeName(componentName);
    const dest = handleSame(dirPath, normalized.baseName);
    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-package.vue'), dest);

    if (normalized.baseName !== 'u-sample') {
        await batchReplace([
            path.join(dest, 'index.js'),
            path.join(dest, 'README.md'),
            path.join(dest, 'package.json'),
        ], [
            [/u-sample/g, normalized.baseName],
            [/USample/g, normalized.componentName],
        ]);
    }
    return dest;
}

export async function createPage(dirPath: string) {
    const dest = handleSame(dirPath, 'page');
    await fs.copy(path.resolve(__dirname, '../../templates/page.vue'), dest);
    return dest;
}

export async function createListPage(dirPath: string) {
    const dest = handleSame(dirPath, 'list');
    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-subdocs.vue'), dest);
    return dest;
}

export async function createFormPage(dirPath: string) {
    const dest = handleSame(dirPath, 'form');
    await fs.copy(path.resolve(__dirname, '../../templates/page.vue'), dest);
    return dest;
}

export async function createDetailPage(dirPath: string) {
    const dest = handleSame(dirPath, 'detail');
    await fs.copy(path.resolve(__dirname, '../../templates/page.vue'), dest);
    return dest;
}

export async function addDoc(vuePath: string) {
    if (!fs.statSync(vuePath).isDirectory())
        throw new Error('Unsupport adding blocks in single vue file!');

    const dest = path.resolve(vuePath, 'README.md');
    if (fs.existsSync(dest))
        throw new FileExistsError('File README.md exists!');

    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file.vue/README.md'), dest);

    const baseName = path.basename(vuePath, path.extname(vuePath));
    const componentName = kebab2Camel(baseName);
    await batchReplace(dest, [
        [/u-sample/g, baseName],
        [/USample/g, componentName],
    ]);
    return dest;
}

export async function addDocWithSubs(vuePath: string) {
    if (!fs.statSync(vuePath).isDirectory())
        throw new Error('Unsupport adding blocks in single vue file!');
    const baseName = path.basename(vuePath, path.extname(vuePath));
    const componentName = kebab2Camel(baseName);

    const dest = path.resolve(vuePath, 'README.md');
    if (fs.existsSync(dest))
        throw new FileExistsError('File "README.md" exists!');

    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-subdocs.vue/README.md'), dest);
    await batchReplace(dest, [
        [/u-sample/g, baseName],
        [/USample/g, componentName],
    ]);

    const dest2 = path.resolve(vuePath, 'docs');
    if (fs.existsSync(dest2))
        throw new FileExistsError('Directory "docs/" exists!');

    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-subdocs.vue/docs'), dest2);
    await batchReplace([
        path.join(dest, 'api.md'),
        path.join(dest, 'examples.md'),
    ], [
        [/u-sample/g, baseName],
        [/USample/g, componentName],
    ]);

    return dest;
}

export async function addDocWithScreenshots(vuePath: string) {
    if (!fs.statSync(vuePath).isDirectory())
        throw new Error('Unsupport adding blocks in single vue file!');

    const dest = path.resolve(vuePath, 'README.md');
    if (fs.existsSync(dest))
        throw new Error('File README.md exists!');

    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-screenshots.vue/README.md'), dest);

    const baseName = path.basename(vuePath, path.extname(vuePath));
    const componentName = kebab2Camel(baseName);
    await batchReplace(dest, [
        [/u-sample/g, baseName],
        [/USample/g, componentName],
    ]);

    const dest2 = path.resolve(vuePath, 'screenshots');
    if (fs.existsSync(dest2))
        throw new Error('Directory "screenshots/" exists!');

    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file-with-screenshots.vue/screenshots'), dest2);

    return dest;
}

export async function addModuleCSS(vuePath: string) {
    if (!fs.statSync(vuePath).isDirectory())
        throw new Error('Unsupport adding blocks in single vue file!');

    const dest = path.resolve(vuePath, 'module.css');
    if (fs.existsSync(dest))
        throw new Error('File module.css exists!');

    await fs.copy(path.resolve(__dirname, '../../templates/u-multi-file.vue/module.css'), dest);
    return dest;
}

/**
 * 扩展到新的路径中
 * @param vueFile 原组件库需要扩展的组件，一级、二级组件均可
 * @param from 原来的库，或者 VueFile 本身的路径
 * @param to 新的路径
 */
export async function extendToPath(vueFile: VueFile, from: Library | string, to: string, mode: VueFileExtendMode) {
    let importFrom: string;
    if (from instanceof Library) {
        importFrom = from.fileName;
    } else {
        importFrom = from;
    }

    const dest = to;
    const destDir = path.dirname(dest);

    if (fs.existsSync(dest))
        throw new FileExistsError(dest);
    if (!fs.existsSync(destDir))
        fs.mkdirSync(destDir);

    const newVueFile = vueFile.extend(mode, dest, importFrom);
    await newVueFile.save();

    return newVueFile;
}

/**
 * 扩展到新的库中
 * @param vueFile 原组件库需要扩展的组件，一级、二级组件均可
 * @param from 原来的库，或者 VueFile 本身的路径
 * @param to 需要扩展到的组件库，比如 internalLibrary
 */
export async function extendToLibrary(vueFile: VueFile, from: Library | string, to: Library, mode: VueFileExtendMode, subDir?: string) {
    let importFrom: string;
    if (from instanceof Library) {
        if (subDir === undefined)
            subDir = to.config.type !== 'library' ? from.baseName : ''; // @example 'cloud-ui';
        importFrom = from.fileName;
    } else {
        if (subDir === undefined)
            subDir = to.config.type !== 'library' ? 'other' : '';
        importFrom = from;
    }

    const arr = vueFile.fullPath.split(path.sep);
    let pos = arr.length - 1; // root Vue 的位置
    while(arr[pos] && arr[pos].endsWith('.vue'))
        pos--;
    pos++;
    const basePath = arr.slice(0, pos).join(path.sep);
    const fromRelativePath = path.relative(basePath, vueFile.fullPath);
    const toRelativePath = subDir ? `./${subDir}/${fromRelativePath}` : `./${fromRelativePath}`;
    const toPath = to.componentsDirectory.fullPath;

    const destDir = path.resolve(toPath, subDir);
    const dest = path.resolve(toPath, toRelativePath);
    const parentDest = path.dirname(dest);

    // 如果为子组件，且父组件不存在的话，先创建父组件
    if (vueFile.isChild && !fs.existsSync(parentDest))
        await extendToLibrary(vueFile.parent, from, to, VueFileExtendMode.script, subDir);

    if (fs.existsSync(dest))
        throw new FileExistsError(dest);
    if (!fs.existsSync(destDir))
        fs.mkdirSync(destDir);

    const newVueFile = vueFile.extend(mode, dest, importFrom);
    await newVueFile.save();

    // 子组件在父组件中添加，根组件在 index.js 中添加
    if (vueFile.isChild) {
        // VueFile.save() 会清掉子组件
        // const parentFile = new VueFile(parentDest);
        // await parentFile.open();
        // parentFile.parseScript();
        const parentIndexFile = JSFile.fetch(path.join(parentDest, 'index.js'));
        await parentIndexFile.open();
        parentIndexFile.parse();

        await vueFile.open();
        vueFile.parseScript();

        const relativePath = './' + vueFile.fileName;

        // const getExportSpecifiers = () => {
        const exportNames: Array<string> = [];
        babel.traverse(vueFile.scriptHandler.ast, {
            ExportNamedDeclaration(nodePath) {
                if (nodePath.node.declaration) {
                    (nodePath.node.declaration as babel.types.VariableDeclaration).declarations.forEach((declaration) => {
                        exportNames.push((declaration.id as babel.types.Identifier).name);
                    });
                }

                if (nodePath.node.specifiers) {
                    nodePath.node.specifiers.forEach((specifier) => {
                        exportNames.push(specifier.exported.name);
                    });
                }
            },
        });
        // }

        const createExportNamed = () => {
            const exportNamedDeclaration = babel.template(`export { ${exportNames.join(', ')} } from "${relativePath}"`)() as babel.types.ExportNamedDeclaration;
            // 要逃避 typescript
            // Object.assign(exportNamedDeclaration.source, { raw: `'${relativePath}'` });
            return exportNamedDeclaration;
        }

        let exportNamed: babel.types.ExportNamedDeclaration;
        babel.traverse(parentIndexFile.handler.ast, {
            enter(nodePath) {
                // 只遍历顶级节点
                if (nodePath.parentPath && nodePath.parentPath.isProgram())
                    nodePath.skip();

                if (nodePath.isExportAllDeclaration() || nodePath.isExportNamedDeclaration()) {
                    if (!nodePath.node.source) {
                        // 有可能是 declarations
                    } else if (relativePath === nodePath.node.source.value) {
                        if (nodePath.isExportAllDeclaration) {
                            exportNamed = createExportNamed();
                            nodePath.replaceWith(exportNamed);
                        } else {
                            // exportNamed = nodePath.node;
                        }
                        nodePath.stop();
                    } else if (relativePath < nodePath.node.source.value) {
                        exportNamed = createExportNamed();
                        nodePath.insertBefore(exportNamed);
                        nodePath.stop();
                    }
                } else if (nodePath.isExportDefaultDeclaration() && !exportNamed) {
                    exportNamed = createExportNamed();
                    nodePath.insertBefore(exportNamed);
                    nodePath.stop();
                }
            },
        });

        await parentIndexFile.save();
    } else if (to.componentsIndexFile) {
        const indexFile = to.componentsIndexFile;
        await indexFile.open();
        indexFile.parse();

        const createExportAll = () => {
            const exportAllDeclaration = babel.types.exportAllDeclaration(babel.types.stringLiteral(toRelativePath));
            // 要逃避 typescript
            Object.assign(exportAllDeclaration.source, { raw: `'${toRelativePath}'` });
            return exportAllDeclaration;
        }

        let exportAll: babel.types.ExportAllDeclaration;
        babel.traverse(indexFile.handler.ast, {
            enter(nodePath) {
                // 只遍历顶级节点
                if (nodePath.parentPath && nodePath.parentPath.isProgram())
                    nodePath.skip();

                if (nodePath.isExportAllDeclaration()) {
                    if (!nodePath.node.source) {
                        // 有可能是 declarations
                    } else if (toRelativePath === nodePath.node.source.value) {
                        exportAll = nodePath.node;
                        nodePath.stop();
                    } else if (toRelativePath < nodePath.node.source.value) {
                        exportAll = createExportAll();
                        nodePath.insertBefore(exportAll);
                        nodePath.stop();
                    }
                }
            },
            exit(nodePath) {
                if (nodePath.isProgram() && !exportAll) {
                    exportAll = createExportAll();
                    nodePath.node.body.push(exportAll);
                }
            },
        });

        await indexFile.save();
    }

    return newVueFile;
}
