import * as ts from 'typescript';
import { Tree, UpdateRecorder } from '@angular-devkit/schematics';

/* istanbul ignore file */
/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
export interface Host {
  write(path: string, content: string): Promise<void>;
  read(path: string): Promise<string>;
}

export interface Change {

  // The file this change should be applied to. Some changes might not apply to
  // a file (maybe the config).
  readonly path: string | null;

  // The order this change should be applied. Normally the position inside the file.
  // Changes are applied from the bottom of a file to the top.
  readonly order: number;

  // The description of this change. This will be outputted in a dry or verbose run.
  readonly description: string;

  apply(host: Host): Promise<void>;
}

/**
 * An operation that does nothing.
 */
export class NoopChange implements Change {
  description = 'No operation.';
  order = Infinity;
  path = null;
  apply(): Promise<void> {
    return Promise.resolve();
  }
}

/**
 * Will add text to the source code.
 */
export class InsertChange implements Change {
  order: number;
  description: string;

  constructor(public path: string, public pos: number, public toAdd: string) {
    if (pos < 0) {
      throw new Error('Negative positions are invalid');
    }
    this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
    this.order = pos;
  }

  /**
   * This method does not insert spaces if there is none in the original string.
   */
  apply(host: Host): Promise<void> {
    return host.read(this.path).then(content => {
      const prefix = content.substring(0, this.pos);
      const suffix = content.substring(this.pos);

      return host.write(this.path, `${prefix}${this.toAdd}${suffix}`);
    });
  }
}

/**
 * Will remove text from the source code.
 */
export class RemoveChange implements Change {
  order: number;
  description: string;

  constructor(public path: string, public pos: number, public end: number) {
    if (pos < 0 || end < 0) {
      throw new Error('Negative positions are invalid');
    }
    this.description = `Removed text in position ${pos} to ${end} of ${path}`;
    this.order = pos;
  }

  apply(host: Host): Promise<void> {
    return host.read(this.path).then(content => {
      const prefix = content.substring(0, this.pos);
      const suffix = content.substring(this.end);

      // TODO: throw error if toRemove doesn't match removed string.
      return host.write(this.path, `${prefix}${suffix}`);
    });
  }
}

/**
 * Will replace text from the source code.
 */
export class ReplaceChange implements Change {
  order: number;
  description: string;

  constructor(public path: string, public pos: number, public oldText: string, public newText: string) {
    if (pos < 0) {
      throw new Error('Negative positions are invalid');
    }
    this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
    this.order = pos;
  }

  apply(host: Host): Promise<void> {
    return host.read(this.path).then(content => {
      const prefix = content.substring(0, this.pos);
      const suffix = content.substring(this.pos + this.oldText.length);
      const text = content.substring(this.pos, this.pos + this.oldText.length);

      if (text !== this.oldText) {
        return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`));
      }

      // TODO: throw error if oldText doesn't match removed string.
      return host.write(this.path, `${prefix}${this.newText}${suffix}`);
    });
  }
}

export function createReplaceChange(
  sourceFile: ts.SourceFile,
  node: ts.Node,
  oldText: string,
  newText: string
): ReplaceChange {
  return new ReplaceChange(sourceFile.fileName, node.getStart(sourceFile), oldText, newText);
}

export function createChangeRecorder(tree: Tree, path: string, changes: Change[]): UpdateRecorder {
  const recorder = tree.beginUpdate(path);
  for (const change of changes) {
    if (change instanceof InsertChange) {
      recorder.insertLeft(change.pos, change.toAdd);
    } else if (change instanceof RemoveChange) {
      recorder.remove(change.pos, change.end - change.pos);
    } else if (change instanceof ReplaceChange) {
      recorder.remove(change.pos, change.oldText.length);
      recorder.insertLeft(change.pos, change.newText);
    }
  }
  return recorder;
}

export function commitChanges(tree: Tree, path: string, changes: Change[]): boolean {
  if (changes.length === 0) {
    return false;
  }

  const recorder = createChangeRecorder(tree, path, changes);
  tree.commitUpdate(recorder);
  return true;
}