/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this 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.
 */
/**
 * @fileoverview Standardized Firebase Error.
 *
 * Usage:
 *
 *   // Typescript string literals for type-safe codes
 *   type Err =
 *     'unknown' |
 *     'object-not-found'
 *     ;
 *
 *   // Closure enum for type-safe error codes
 *   // at-enum {string}
 *   var Err = {
 *     UNKNOWN: 'unknown',
 *     OBJECT_NOT_FOUND: 'object-not-found',
 *   }
 *
 *   let errors: Map<Err, string> = {
 *     'generic-error': "Unknown error",
 *     'file-not-found': "Could not find file: {$file}",
 *   };
 *
 *   // Type-safe function - must pass a valid error code as param.
 *   let error = new ErrorFactory<Err>('service', 'Service', errors);
 *
 *   ...
 *   throw error.create(Err.GENERIC);
 *   ...
 *   throw error.create(Err.FILE_NOT_FOUND, {'file': fileName});
 *   ...
 *   // Service: Could not file file: foo.txt (service/file-not-found).
 *
 *   catch (e) {
 *     assert(e.message === "Could not find file: foo.txt.");
 *     if (e.code === 'service/file-not-found') {
 *       console.log("Could not read file: " + e['file']);
 *     }
 *   }
 */
export type ErrorList<T> = { [code: string]: string };
 
const ERROR_NAME = 'FirebaseError';
 
export interface StringLike {
  toString: () => string;
}
 
let captureStackTrace: (obj: Object, fn?: Function) => void = (Error as any)
  .captureStackTrace;
 
// Export for faking in tests
export function patchCapture(captureFake?: any): any {
  let result: any = captureStackTrace;
  captureStackTrace = captureFake;
  return result;
}
 
export interface FirebaseError {
  // Unique code for error - format is service/error-code-string
  code: string;
 
  // Developer-friendly error message.
  message: string;
 
  // Always 'FirebaseError'
  name: string;
 
  // Where available - stack backtrace in a string
  stack: string;
}
 
export class FirebaseError implements FirebaseError {
  public stack: string;
  public name: string;
 
  constructor(public code: string, public message: string) {
    let stack: string;
    // We want the stack value, if implemented by Error
    if (captureStackTrace) {
      // Patches this.stack, omitted calls above ErrorFactory#create
      captureStackTrace(this, ErrorFactory.prototype.create);
    } else {
      let err = Error.apply(this, arguments);
      this.name = ERROR_NAME;
      // Make non-enumerable getter for the property.
      Object.defineProperty(this, 'stack', {
        get: function() {
          return err.stack;
        }
      });
    }
  }
}
 
// Back-door inheritance
FirebaseError.prototype = Object.create(Error.prototype) as FirebaseError;
FirebaseError.prototype.constructor = FirebaseError;
(FirebaseError.prototype as any).name = ERROR_NAME;
 
export class ErrorFactory<T extends string> {
  // Matches {$name}, by default.
  public pattern = /\{\$([^}]+)}/g;
 
  constructor(
    private service: string,
    private serviceName: string,
    private errors: ErrorList<T>
  ) {
    // empty
  }
 
  create(code: T, data?: { [prop: string]: StringLike }): FirebaseError {
    if (data === undefined) {
      data = {};
    }
 
    let template = this.errors[code as string];
 
    let fullCode = this.service + '/' + code;
    let message: string;
 
    if (template === undefined) {
      message = 'Error';
    } else {
      message = template.replace(this.pattern, (match, key) => {
        let value = data![key];
        return value !== undefined ? value.toString() : '<' + key + '?>';
      });
    }
 
    // Service: Error message (service/code).
    message = this.serviceName + ': ' + message + ' (' + fullCode + ').';
    let err = new FirebaseError(fullCode, message);
 
    // Populate the Error object with message parts for programmatic
    // accesses (e.g., e.file).
    for (let prop in data) {
      if (!data.hasOwnProperty(prop) || prop.slice(-1) === '_') {
        continue;
      }
      (err as any)[prop] = data[prop];
    }
 
    return err;
  }
}
  |