import { google } from '../build/pbjs';
import FileDescriptorProto = google.protobuf.FileDescriptorProto;

/** This type is expecting a value from the Fields constant. */
export type FieldID = number;

/**
 * The field values here represent the proto field IDs associated with the types
 * (file,message,enum,service).
 *
 * For more information read the comments for SourceCodeInfo declared in
 * google's 'descriptor.proto' file, see:
 * https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto#L730
 */
export const Fields = {
  file: {
    syntax: 12,
    message_type: 4,
    enum_type: 5,
    service: 6,
    extension: 7,
  },
  message: {
    field: 2,
    nested_type: 3,
    enum_type: 4,
    oneof_decl: 8,
  },
  enum: {
    value: 2,
  },
  service: {
    method: 2,
  },
};

/**
 * This type is simply an interface on the SourceCodeInfo.Location message.
 */
export interface SourceDescription {
  readonly span: number[];
  readonly leadingComments: string;
  readonly trailingComments: string;
  readonly leadingDetachedComments: string[];
}

/** An empty SourceDescription for when one is not available. */
class EmptyDescription implements SourceDescription {
  span = [];
  leadingComments = '';
  trailingComments = '';
  leadingDetachedComments = [];
}

/**
 * Mapping from a string of dotted notation `path` parts to efficiently
 * lookup the related source information.
 */
export type SourceInfoMap = {
  [key: string]: SourceDescription;
};

/**
 * This class provides direct lookup and navigation through the type
 * system by the use of lookup/open to access the source info for types
 * defined in a protocol buffer.
 */
export default class SourceInfo implements SourceDescription {
  /** Returns an empty SourceInfo */
  static empty() {
    return new SourceInfo({}, new EmptyDescription());
  }

  /**
   * Creates the SourceInfo from the FileDescriptorProto given to you
   * by the protoc compiler. It indexes file.sourceCodeInfo by dotted
   * path notation and returns the root SourceInfo.
   */
  static fromDescriptor(file: FileDescriptorProto) {
    let map: SourceInfoMap = {};
    if (file.sourceCodeInfo && file.sourceCodeInfo.location) {
      file.sourceCodeInfo.location.forEach((loc) => {
        map[loc.path.join('.')] = loc;
      });
    }
    return new SourceInfo(map, new EmptyDescription());
  }

  // Private
  private constructor(
    private readonly sourceCode: SourceInfoMap,
    private readonly selfDescription: SourceDescription
  ) {}

  /** Returns the code span [start line, start column, end line] */
  get span() {
    return this.selfDescription.span;
  }

  /** Leading consecutive comment lines prior to the current element */
  get leadingComments() {
    return this.selfDescription.leadingComments;
  }

  /** Documentation is unclear about what exactly this is */
  get trailingComments() {
    return this.selfDescription.trailingComments;
  }

  /** Detached comments are those preceeding but separated by a blank non-comment line */
  get leadingDetachedComments() {
    return this.selfDescription.leadingDetachedComments;
  }

  /** Return the source info for the field id and index specficied */
  lookup(type: FieldID, index?: number): SourceDescription {
    if (index === undefined) {
      return this.sourceCode[`${type}`] || new EmptyDescription();
    }
    return this.sourceCode[`${type}.${index}`] || new EmptyDescription();
  }

  /** Returns a new SourceInfo class representing the field id and index specficied */
  open(type: FieldID, index: number): SourceInfo {
    const prefix = `${type}.${index}.`;
    const map: SourceInfoMap = {};
    Object.keys(this.sourceCode)
      .filter((key) => key.startsWith(prefix))
      .forEach((key) => {
        map[key.substr(prefix.length)] = this.sourceCode[key];
      });
    return new SourceInfo(map, this.lookup(type, index));
  }
}
