// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {isString} from '../../base/object_utils';
import {exists} from '../../base/utils';

export type Key = string | number;

export interface ArgNode<T> {
  key: Key;
  value?: T;
  children?: ArgNode<T>[];
}

// Arranges a flat list of arg-like objects (objects with a string "key" value
// indicating their path) into a nested tree.
//
// This process is relatively forgiving as it allows nodes with both values and
// child nodes as well as children with mixed key types in the same node.
//
// When duplicate nodes exist, the latest one is picked.
//
// If you want to convert args to a POJO, try convertArgsToObject().
//
// Key should be a path separated by periods (.) or indexes specified using a
// number inside square brackets.
// e.g. foo.bar[0].x
//
// See unit tests for examples.
export function convertArgsToTree<T extends {key: string}>(
  input: ReadonlyArray<T>,
): ArgNode<T>[] {
  const result: ArgNode<T>[] = [];
  for (const arg of input) {
    const {key} = arg;
    const nestedKey = getNestedKey(key);
    insert(result, nestedKey, key, arg);
  }
  return result;
}

function getNestedKey(key: string): Key[] {
  const result: Key[] = [];
  let match;
  const re = /([^\.\[\]]+)|\[(\d+)\]/g;
  while ((match = re.exec(key)) !== null) {
    result.push(match[2] ? parseInt(match[2]) : match[1]);
  }
  return result;
}

function insert<T>(
  args: ArgNode<T>[],
  keys: Key[],
  path: string,
  value: T,
): void {
  const currentKey = keys.shift()!;
  let node = args.find((x) => x.key === currentKey);
  if (!node) {
    node = {key: currentKey};
    args.push(node);
  }
  if (keys.length > 0) {
    if (node.children === undefined) {
      node.children = [];
    }
    insert(node.children, keys, path, value);
  } else {
    node.value = value;
  }
}

type ArgLike<T> = {
  key: string;
  value: T;
};
type ObjectType<T> = T | ObjectType<T>[] | {[key: string]: ObjectType<T>};

// Converts a list of argument-like objects (i.e. objects with key and value
// fields) to a POJO.
//
// This function cannot handle cases where nodes contain mixed node types (i.e.
// both number and string types) as nodes cannot be both an object and an array,
// and will throw when this situation arises.
//
// Key should be a path separated by periods (.) or indexes specified using a
// number inside square brackets.
// e.g. foo.bar[0].x
//
// See unit tests for examples.
export function convertArgsToObject<A extends ArgLike<T>, T>(
  input: A[],
): ObjectType<T> {
  const nested = convertArgsToTree(input);
  return parseNodes(nested);
}

function parseNodes<A extends ArgLike<T>, T>(
  nodes: ArgNode<A>[],
): ObjectType<T> {
  if (nodes.every(({key}) => isString(key))) {
    const dict: ObjectType<T> = {};
    for (const node of nodes) {
      if (node.key in dict) {
        throw new Error(`Duplicate key ${node.key}`);
      }
      dict[node.key] = parseNode(node);
    }
    return dict;
  } else if (nodes.every(({key}) => typeof key === 'number')) {
    const array: ObjectType<T>[] = [];
    for (const node of nodes) {
      const index = node.key as number;
      if (index in array) {
        throw new Error(`Duplicate array index ${index}`);
      }
      array[index] = parseNode(node);
    }
    return array;
  } else {
    throw new Error('Invalid mix of node key types');
  }
}

function parseNode<A extends ArgLike<T>, T>({
  value,
  children,
}: ArgNode<A>): ObjectType<T> {
  if (exists(value) && !exists(children)) {
    return value.value;
  } else if (!exists(value) && exists(children)) {
    return parseNodes(children);
  } else {
    throw new Error('Invalid node type');
  }
}
