import { strings, normalize } from '@angular-devkit/core';
import { ProjectDefinition } from '@angular-devkit/core/src/workspace';
import {
  apply,
  applyTemplates,
  branchAndMerge,
  chain,
  filter,
  mergeWith,
  move,
  noop,
  Rule,
  SchematicsException,
  Tree,
  url
} from '@angular-devkit/schematics';
import { Schema as ComponentSchema } from '@schematics/angular/component/schema';
import { insertImport, getSourceNodes } from '@schematics/angular/utility/ast-utils';
import { InsertChange } from '@schematics/angular/utility/change';
import { buildRelativePath, findModuleFromOptions, ModuleOptions } from '@schematics/angular/utility/find-module';
import { parseName } from '@schematics/angular/utility/parse-name';
import { validateHtmlSelector } from '@schematics/angular/utility/validation';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';

import { addServiceToModuleOrStandalone, findRoutesPath, getSourceFile, ROUTINS_FILENAME } from './ast';
import { isStandalone } from './standalone';
import { getProject, NgAlainProjectDefinition } from './workspace';

export interface CommonSchema extends ComponentSchema {
  [key: string]: any;
  _filesPath?: string;
  schematicName?: string;
  target?: string;
  componentName?: string;
  serviceName?: string;
  importModulePath?: string;
  routerModulePath?: string;
  selector?: string;
  withoutPrefix?: boolean;
  withoutModulePrefixInComponentName?: boolean;
  skipTests?: boolean;
  service?: 'ignore' | 'none' | 'root';
  flat?: boolean;
  modal?: boolean;
}

function buildSelector(schema: CommonSchema, projectPrefix: string): string {
  const ret: string[] = [];
  if (!schema.withoutPrefix) {
    if (schema.prefix) {
      ret.push(schema.prefix);
    } else if (schema.prefix === undefined && projectPrefix) {
      ret.push(projectPrefix);
    }
  }
  // module name
  if (schema.module) {
    ret.push(schema.module);
  }
  // target name
  if (schema.target) {
    ret.push(...schema.target.split('/'));
  }
  // name
  ret.push(strings.dasherize(schema.name!));
  return ret.join('-');
}

function buildName(schema: CommonSchema, prefix: 'Component' | 'Service'): string {
  const ret: string[] = schema.withoutModulePrefixInComponentName === true ? [] : [schema.module!];
  if (schema.target && schema.target.length > 0) {
    ret.push(...schema.target.split('/'));
  }
  ret.push(schema.name!);
  // 服务类自动过滤 list, empty 两个页面的后缀
  if (prefix === 'Service' && ['list', 'empty'].includes(schema.name)) {
    ret.pop();
  }
  ret.push(prefix);
  return strings.classify(ret.join('-'));
}

export function refreshPathRoot(
  project: ProjectDefinition,
  schema: CommonSchema,
  alainProject: NgAlainProjectDefinition
): void {
  if (schema.path === undefined) {
    schema.path = `/${path.join(project.sourceRoot!, alainProject?.routesRoot ?? 'app/routes')}`;
  }
}

function resolveSchema(
  tree: Tree,
  project: ProjectDefinition,
  schema: CommonSchema,
  alainProject: NgAlainProjectDefinition
): void {
  // module name
  if (!schema.module) {
    throw new SchematicsException(`Must specify module name. (e.g: ng g ng-alain:list <list name> -m=<module name>)`);
  }
  // path
  refreshPathRoot(project, schema, alainProject);

  schema.path += `/${schema.module}`;

  const parsedPath = parseName(schema.path, schema.name!);
  schema.name = parsedPath.name;
  schema.path = parsedPath.path;
  const rootPath = path.resolve(__dirname, '../../..');
  const fullPath = path.join(rootPath, schema.path, schema.name);
  if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length > 0) {
    throw new SchematicsException(`The directory (${fullPath}) already exists`);
  }

  if (schema.standalone) {
    schema.importModulePath = normalize(`${schema.path}/${schema.name}/${schema.name}.component.ts`);
  } else {
    schema.importModulePath = findModuleFromOptions(tree, schema as unknown as ModuleOptions);
  }

  if (!schema._filesPath) {
    // 若基础页尝试从 `_cli-tpl/_${schema.schematicName!}` 下查找该目录，若存在则优先使用
    if (['list', 'edit', 'view', 'empty'].includes(schema.schematicName!)) {
      const overrideDir = `/${[project.root, `_cli-tpl/_${schema.schematicName}`].filter(i => !!i).join('/')}`;
      const overridePath = `${overrideDir}/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.ts`;
      if (tree.exists(overridePath) || tree.exists(`${overridePath}.template`)) {
        // 所在目录与命令目录同属一个目录结构，因此无须特殊处理
        schema._filesPath = path.relative(__dirname, rootPath) + overrideDir;
      }
    }
    schema._filesPath = schema._filesPath || './files';
  }
  // fill target
  if (schema.target) {
    schema.path += strings.dasherize(`/${schema.target}`);
  }

  if (schema.standalone) {
    schema.routerModulePath = findRoutesPath(tree, schema.path);
    if (schema.routerModulePath.length <= 0) {
      throw new SchematicsException(`Could not find a non Routing file: ${ROUTINS_FILENAME}`);
    }
  } else {
    schema.routerModulePath = schema.importModulePath!.replace('.module.ts', '-routing.module.ts');
  }

  // html selector
  schema.selector = schema.selector || buildSelector(schema, project.prefix);

  validateHtmlSelector(schema.selector);
}

export function addImportToModule(tree: Tree, filePath: string, symbolName: string, fileName: string): void {
  const source = getSourceFile(tree, filePath);
  const change = insertImport(source, filePath, symbolName, fileName) as InsertChange;
  if (change.path == null) return;
  const declarationRecorder = tree.beginUpdate(filePath);
  declarationRecorder.insertLeft(change.pos, change.toAdd);
  tree.commitUpdate(declarationRecorder);
}

export function addValueToVariable(
  tree: Tree,
  filePath: string,
  variableName: string,
  text: string,
  needWrap: boolean = true
): void {
  const source = getSourceFile(tree, filePath);
  const node = getSourceNodes(source).find(
    node => node.kind == ts.SyntaxKind.Identifier && node.getText() === variableName
  );
  if (!node) {
    throw new SchematicsException(`Could not find any [${variableName}] variable in path '${filePath}'.`);
  }

  const arr = (node.parent as any).initializer as ts.ArrayLiteralExpression;

  const change = new InsertChange(
    filePath,
    arr.end - 1,
    `${arr.elements && arr.elements.length > 0 ? ',' : ''}${needWrap ? '\n  ' : ''}${text}`
  );

  const declarationRecorder = tree.beginUpdate(filePath);
  declarationRecorder.insertLeft(change.pos, change.toAdd);
  tree.commitUpdate(declarationRecorder);
}

function getRelativePath(filePath: string, schema: CommonSchema, prefix: 'component' | 'service'): string {
  const importPath = normalize(
    `/${schema.path}/${schema.flat ? '' : `${strings.dasherize(schema.name!)}/`}${strings.dasherize(
      schema.name!
    )}.${prefix}`
  );
  return buildRelativePath(filePath, importPath);
}

function addDeclaration(schema: CommonSchema): Rule {
  return (tree: Tree) => {
    if (schema.skipImport || !schema.module) {
      return tree;
    }

    // imports
    if (!schema.standalone) {
      addImportToModule(
        tree,
        schema.importModulePath!,
        schema.componentName!,
        getRelativePath(schema.importModulePath!, schema, 'component')
      );
      addValueToVariable(tree, schema.importModulePath!, 'COMPONENTS', schema.componentName!);
    }

    // component
    if (schema.modal !== true) {
      // routing
      addImportToModule(
        tree,
        schema.routerModulePath!,
        schema.componentName!,
        getRelativePath(schema.routerModulePath!, schema, 'component')
      );
      addValueToVariable(
        tree,
        schema.routerModulePath!,
        'routes',
        `{ path: '${schema.name}', component: ${schema.componentName} }`
      );
    }

    // service
    if (schema.service === 'none') {
      addServiceToModuleOrStandalone(
        tree,
        schema.standalone,
        schema.importModulePath!,
        schema.serviceName!,
        getRelativePath(schema.importModulePath!, schema, 'service')
      );
    }

    return tree;
  };
}

export function buildAlain(schema: CommonSchema): Rule {
  return async (tree: Tree) => {
    const res = await getProject(tree, schema.project);
    if (schema.project && res.name !== schema.project) {
      throw new SchematicsException(`The specified project does not match '${schema.project}', current: ${res.name}`);
    }
    const project = res.project;

    // standalone
    schema.standalone = await isStandalone(tree, schema.standalone, res.name);

    resolveSchema(tree, project, schema, res.alainProject);

    schema.componentName = buildName(schema, 'Component');
    schema.serviceName = buildName(schema, 'Service');

    // Don't support inline
    schema.inlineTemplate = false;

    const templateSource = apply(url(schema._filesPath!), [
      filter(filePath => !filePath.endsWith('.DS_Store')),
      schema.service === 'ignore' ? filter(filePath => !filePath.endsWith('.service.ts.template')) : noop(),
      schema.skipTests ? filter(filePath => !filePath.endsWith('.spec.ts.template')) : noop(),
      schema.inlineStyle ? filter(filePath => !filePath.endsWith('.__style__.template')) : noop(),
      schema.inlineTemplate ? filter(filePath => !filePath.endsWith('.html.template')) : noop(),
      // schema.spec ? noop() : filter(filePath => !filePath.endsWith('.spec.ts')),
      // schema.inlineStyle ? filter(filePath => !filePath.endsWith('.__styleext__')) : noop(),
      // schema.inlineTemplate ? filter(filePath => !filePath.endsWith('.html')) : noop(),
      applyTemplates({
        ...strings,
        'if-flat': (s: string) => (schema.flat ? '' : s),
        ...schema
      }),
      move(null!, `${schema.path}/`)
    ]);

    return chain([branchAndMerge(chain([mergeWith(templateSource), addDeclaration(schema)]))]);
  };
}
