text: import { dirname, resolve } from 'path';
  import { readFile, readFileSync, Promise } from 'sander';
  import { decode } from 'sourcemap-codec';
  import getMap from './utils/getMap.js';
  
  export default function Node ({ file, content }) {
  	this.file = file ? resolve( file ) : null;
  	this.content = content || null; // sometimes exists in sourcesContent, sometimes doesn't
  
  	if ( !this.file && this.content === null ) {
  		throw new Error( 'A source must specify either file or content' );
  	}
  
  	// these get filled in later
  	this.map = null;
  	this.mappings = null;
  	this.sources = null;
  	this.isOriginalSource = null;
  
  	this._stats = {
  		decodingTime: 0,
  		encodingTime: 0,
  		tracingTime: 0,
  
  		untraceable: 0
  	};
  }
  
  Node.prototype = {
  	load ( sourcesContentByPath, sourceMapByPath ) {
  		return getContent( this, sourcesContentByPath ).then( content => {
  			this.content = sourcesContentByPath[ this.file ] = content;
  
  			return getMap( this, sourceMapByPath ).then( map => {
  				if ( !map ) return null;
  
  				this.map = map;
  
  				let decodingStart = process.hrtime();
  				this.mappings = decode( map.mappings );
  				let decodingTime = process.hrtime( decodingStart );
  				this._stats.decodingTime = 1e9 * decodingTime[0] + decodingTime[1];
  
  				const sourcesContent = map.sourcesContent || [];
  
  				const sourceRoot = resolve( dirname( this.file ), map.sourceRoot || '' );
  
  				this.sources = map.sources.map( ( source, i ) => {
  					return new Node({
  						file: source ? resolve( sourceRoot, source ) : null,
  						content: sourcesContent[i]
  					});
  				});
  
  				const promises = this.sources.map( node => node.load( sourcesContentByPath, sourceMapByPath ) );
  				return Promise.all( promises );
  			});
  		});
  	},
  
  	loadSync ( sourcesContentByPath, sourceMapByPath ) {
  		if ( !this.content ) {
  			if ( !sourcesContentByPath[ this.file ] ) {
  				sourcesContentByPath[ this.file ] = readFileSync( this.file, { encoding: 'utf-8' });
  			}
  
  			this.content = sourcesContentByPath[ this.file ];
  		}
  
  		const map = getMap( this, sourceMapByPath, true );
  		let sourcesContent;
  
  		if ( !map ) {
  			this.isOriginalSource = true;
  		} else {
  			this.map = map;
  			this.mappings = decode( map.mappings );
  
  			sourcesContent = map.sourcesContent || [];
  
  			const sourceRoot = resolve( dirname( this.file ), map.sourceRoot || '' );
  
  			this.sources = map.sources.map( ( source, i ) => {
  				const node = new Node({
  					file: resolve( sourceRoot, source ),
  					content: sourcesContent[i]
  				});
  
  				node.loadSync( sourcesContentByPath, sourceMapByPath );
  				return node;
  			});
  		}
  	},
  
  	/**
  	 * Traces a segment back to its origin
  	 * @param {number} lineIndex - the zero-based line index of the
  	   segment as found in `this`
  	 * @param {number} columnIndex - the zero-based column index of the
  	   segment as found in `this`
  	 * @param {string || null} - if specified, the name that should be
  	   (eventually) returned, as it is closest to the generated code
  	 * @returns {object}
  	     @property {string} source - the filepath of the source
  	     @property {number} line - the one-based line index
  	     @property {number} column - the zero-based column index
  	     @property {string || null} name - the name corresponding
  	     to the segment being traced
  	 */
  	trace ( lineIndex, columnIndex, name ) {
  		// If this node doesn't have a source map, we have
  		// to assume it is the original source
  		if ( this.isOriginalSource ) {
  			return {
  				source: this.file,
  				line: lineIndex + 1,
  				column: columnIndex || 0,
  				name: name
  			};
  		}
  
  		// Otherwise, we need to figure out what this position in
  		// the intermediate file corresponds to in *its* source
  		const segments = this.mappings[ lineIndex ];
  
  		if ( !segments || segments.length === 0 ) {
  			return null;
  		}
  
  		if ( columnIndex != null ) {
  			let len = segments.length;
  			let i;
  
  			for ( i = 0; i < len; i += 1 ) {
  				let generatedCodeColumn = segments[i][0];
  
  				if ( generatedCodeColumn > columnIndex ) {
  					break;
  				}
  
  				if ( generatedCodeColumn === columnIndex ) {
  					if ( segments[i].length < 4 ) return null;
  
  					let sourceFileIndex = segments[i][1];
  					let sourceCodeLine = segments[i][2];
  					let sourceCodeColumn = segments[i][3];
  					let nameIndex = segments[i][4];
  
  					let parent = this.sources[ sourceFileIndex ];
  					return parent.trace( sourceCodeLine, sourceCodeColumn, this.map.names[ nameIndex ] || name );
  				}
  			}
  		}
  
  		// fall back to a line mapping
  		let sourceFileIndex = segments[0][1];
  		let sourceCodeLine = segments[0][2];
  		let nameIndex = segments[0][4];
  
  		let parent = this.sources[ sourceFileIndex ];
  		return parent.trace( sourceCodeLine, null, this.map.names[ nameIndex ] || name );
  	}
  };
  
  function getContent ( node, sourcesContentByPath ) {
  	if ( node.file in sourcesContentByPath ) {
  		node.content = sourcesContentByPath[ node.file ];
  	}
  
  	if ( !node.content ) {
  		return readFile( node.file, { encoding: 'utf-8' });
  	}
  
  	return Promise.resolve( node.content );
  }
  