/**
 * @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
 */
import {
  JsonAstArray,
  JsonAstKeyValue,
  JsonAstNode,
  JsonAstObject,
  JsonValue,
} from '@angular-devkit/core';
import { UpdateRecorder } from '@angular-devkit/schematics';

export function appendPropertyInAstObject(
  recorder: UpdateRecorder,
  node: JsonAstObject,
  propertyName: string,
  value: JsonValue,
  indent: number,
) {
  const indentStr = _buildIndent(indent);
  let index = node.start.offset + 1;
  if (node.properties.length > 0) {
    // Insert comma.
    const last = node.properties[node.properties.length - 1];
    const { text, end } = last;
    const commaIndex = text.endsWith('\n') ? end.offset - 1 : end.offset;
    recorder.insertRight(commaIndex, ',');
    index = end.offset;
  }
  const content = _stringifyContent(value, indentStr);
  recorder.insertRight(
    index,
    (node.properties.length === 0 && indent ? '\n' : '')
    + ' '.repeat(indent)
    + `"${propertyName}":${indent ? ' ' : ''}${content}`
    + indentStr.slice(0, -indent),
  );
}

export function insertPropertyInAstObjectInOrder(
  recorder: UpdateRecorder,
  node: JsonAstObject,
  propertyName: string,
  value: JsonValue,
  indent: number,
) {

  if (node.properties.length === 0) {
    appendPropertyInAstObject(recorder, node, propertyName, value, indent);

    return;
  }

  // Find insertion info.
  let insertAfterProp: JsonAstKeyValue | null = null;
  let prev: JsonAstKeyValue | null = null;
  let isLastProp = false;
  const last = node.properties[node.properties.length - 1];
  for (const prop of node.properties) {
    if (prop.key.value > propertyName) {
      if (prev) {
        insertAfterProp = prev;
      }
      break;
    }
    if (prop === last) {
      isLastProp = true;
      insertAfterProp = last;
    }
    prev = prop;
  }

  if (isLastProp) {
    appendPropertyInAstObject(recorder, node, propertyName, value, indent);

    return;
  }

  const indentStr = _buildIndent(indent);
  const insertIndex = insertAfterProp === null
    ? node.start.offset + 1
    : insertAfterProp.end.offset + 1;
  const content = _stringifyContent(value, indentStr);
  recorder.insertRight(
    insertIndex,
    indentStr
    + `"${propertyName}":${indent ? ' ' : ''}${content}`
    + ',',
  );
}

export function removePropertyInAstObject(
  recorder: UpdateRecorder,
  node: JsonAstObject,
  propertyName: string,
) {
  // Find the property inside the object.
  const propIdx = node.properties.findIndex(prop => prop.key.value === propertyName);

  if (propIdx === -1) {
    // There's nothing to remove.
    return;
  }

  if (node.properties.length === 1) {
    // This is a special case. Everything should be removed, including indentation.
    recorder.remove(node.start.offset, node.end.offset - node.start.offset);
    recorder.insertRight(node.start.offset, '{}');

    return;
  }

  // The AST considers commas and indentation to be part of the preceding property.
  // To get around messy comma and identation management, we can work over the range between
  // two properties instead.
  const previousProp = node.properties[propIdx - 1];
  const targetProp = node.properties[propIdx];
  const nextProp = node.properties[propIdx + 1];

  let start, end;
  if (previousProp) {
    // Given the object below, and intending to remove the `m` property:
    // "{\n  \"a\": \"a\",\n  \"m\": \"m\",\n  \"z\": \"z\"\n}"
    //                        ^---------------^
    // Removing the range above results in:
    // "{\n  \"a\": \"a\",\n  \"z\": \"z\"\n}"
    start = previousProp.end;
    end = targetProp.end;
  } else {
    // If there's no previousProp there is a nextProp, since we've specialcased the 1 length case.
    // Given the object below, and intending to remove the `a` property:
    // "{\n  \"a\": \"a\",\n  \"m\": \"m\",\n  \"z\": \"z\"\n}"
    //       ^---------------^
    // Removing the range above results in:
    // "{\n  \"m\": \"m\",\n  \"z\": \"z\"\n}"
    start = targetProp.start;
    end = nextProp.start;
  }

  recorder.remove(start.offset, end.offset - start.offset);
  if (!nextProp) {

    recorder.insertRight(start.offset, '\n');
  }
}


export function appendValueInAstArray(
  recorder: UpdateRecorder,
  node: JsonAstArray,
  value: JsonValue,
  indent = 4,
) {
  const indentStr = _buildIndent(indent);
  let index = node.start.offset + 1;
  if (node.elements.length > 0) {
    // Insert comma.
    const last = node.elements[node.elements.length - 1];
    recorder.insertRight(last.end.offset, ',');
    index = indent ? last.end.offset + 1 : last.end.offset;
  }

  recorder.insertRight(
    index,
    (node.elements.length === 0 && indent ? '\n' : '')
    + ' '.repeat(indent)
    + _stringifyContent(value, indentStr)
    + indentStr.slice(0, -indent),
  );
}


export function findPropertyInAstObject(
  node: JsonAstObject,
  propertyName: string,
): JsonAstNode | null {
  let maybeNode: JsonAstNode | null = null;
  for (const property of node.properties) {
    if (property.key.value == propertyName) {
      maybeNode = property.value;
    }
  }

  return maybeNode;
}

function _buildIndent(count: number): string {
  return count ? '\n' + ' '.repeat(count) : '';
}

function _stringifyContent(value: JsonValue, indentStr: string): string {
  // TODO: Add snapshot tests

  // The 'space' value is 2, because we want to add 2 additional
  // indents from the 'key' node.

  // If we use the indent provided we will have double indents:
  // "budgets": [
  //   {
  //     "type": "initial",
  //     "maximumWarning": "2mb",
  //     "maximumError": "5mb"
  //   },
  //   {
  //       "type": "anyComponentStyle",
  //       'maximumWarning": "5kb"
  //   }
  // ]
  return JSON.stringify(value, null, 2).replace(/\n/g, indentStr);
}
