// Use strict
"use strict";


// Classes

// Identifier class
class Identifier {

	// Public
	
		// Constructor
		constructor(serializedIdentifier = Identifier.DEFAULT_SERIALIZED_IDENTIFIER) {
		
			// Set depth
			this.depth = 0;
			
			// Set paths
			this.paths = new Uint32Array(Identifier.MAX_DEPTH);
			
			// Set value to serialized identifier
			this.setValue(serializedIdentifier);
		}
		
		// Get depth
		getDepth() {
		
			// Return depth
			return this.depth;
		}
		
		// Get paths
		getPaths() {
		
			// Return paths
			return this.paths;
		}
		
		// Set value
		setValue(serializedIdentifierOrDepth, firstValue, secondValue, thirdValue, fourthValue) {
		
			// Check if value is provided in a serialized format
			if(typeof serializedIdentifierOrDepth === "string") {
			
				// Get serialized identifier
				var serializedIdentifier = serializedIdentifierOrDepth;
			
				// Check if serialized identifier ia a hex string
				if(Common.isHexString(serializedIdentifier) === true) {
			
					// Get serialized value
					var serializedValue = Common.fromHexString(serializedIdentifier);
					
					// Check if serialized value is valid
					if(serializedValue["length"] === Identifier.LENGTH) {
					
						// Set depth
						this.depth = Math.min(serializedValue[Identifier.DEPTH_INDEX], Identifier.MAX_DEPTH);
						
						// Go through all paths
						var serializedValueDataView = new DataView(serializedValue["buffer"]);
						
						for(var i = 0; i < this.getPaths()["length"]; ++i)
						
							// Set path
							this.paths[i] = serializedValueDataView.getUint32(Identifier.PATHS_INDEX + i * Uint32Array["BYTES_PER_ELEMENT"], false);
					}
					
					// Otherwise
					else {
					
						// Throw error
						throw "Invalid identifier.";
					}
				}
					
				// Otherwise
				else {
				
					// Throw error
					throw "Invalid identifier.";
				}
			}
			
			// Otherwise
			else {
			
				// Get depth
				var depth = serializedIdentifierOrDepth;
			
				// Check if depth is valid
				if(depth <= Identifier.MAX_DEPTH) {
		
					// Set depth
					this.depth = depth;
					
					// Check if paths are provided as an array
					if(firstValue instanceof Uint32Array === true) {
					
						// Get paths
						var paths = firstValue;
						
						// Check if paths are valid
						if(paths["length"] === this.getPaths()["length"]) {
					
							// Go through all paths
							for(var i = 0; i < this.getPaths()["length"]; ++i) {
							
								// Set path to path
								this.paths[i] = paths[i];
							}
						}
						
						// Otherwise
						else {
						
							// Throw error
							throw "Invalid identifier.";
						}
					}
					
					// Otherwise
					else {
						
						// Set paths
						this.paths[0] = firstValue;
						this.paths[1] = secondValue;
						this.paths[2] = thirdValue;
						this.paths[3] = fourthValue;
					}
				}
				
				// Otherwise
				else {
				
					// Throw error
					throw "Invalid identifier.";
				}
			}
		}
		
		// Get value
		getValue() {
		
			// Initialize buffer
			var buffer = new Uint8Array(Identifier.LENGTH);
			
			// Set buffer's depth
			buffer[Identifier.DEPTH_INDEX] = this.getDepth();
			
			// Go through all paths
			var bufferView = new DataView(buffer["buffer"]);
			
			for(var i = 0; i < this.getPaths()["length"]; ++i)
			
				// Set buffer's path
				bufferView.setUint32(Identifier.PATHS_INDEX + i * Uint32Array["BYTES_PER_ELEMENT"], this.getPaths()[i], false);
			
			// Return buffer
			return buffer;
		}
		
		// Equals value
		equalsValue(identifier) {
		
			// Check if depths differ
			if(this.getDepth() !== identifier.getDepth())
			
				// Return false
				return false;
			
			// Return if paths are equal
			return Common.arraysAreEqual(this.getPaths(), identifier.getPaths() === true);
		}
		
		// Includes value
		includesValue(identifier) {
		
			// Check if depths differ
			if(this.getDepth() !== identifier.getDepth())
			
				// Return false
				return false;
			
			// Return if last path is greater than or equal to the value's last path
			return this.getLastPath() >= identifier.getLastPath();
		}
		
		// Get last path index
		getLastPath() {
		
			// Return last path
			return (this.getDepth() === 0) ? 0 : this.getPaths()[this.getDepth() - 1];
		}
		
		// Get next
		getNext(height = Identifier.NO_HEIGHT) {
		
			// Create next as a clone of self
			var next = this.clone();
			
			// Increment next's last path to value
			++next.paths[next.getDepth() - 1];
			
			// Check if height exists
			if(height !== Identifier.NO_HEIGHT) {
			
				// Check wallet type
				switch(Consensus.getWalletType()) {
				
					// MWC wallet
					case Consensus.MWC_WALLET_TYPE:
				
						// Set next's height path as the height limited by its maximum value
						next.paths[Identifier.HEIGHT_PATH_INDEX] = Math.max(height.modulo(Identifier.MAXIMUM_HEIGHT + 1).toNumber(), 1);
						
						// Break
						break;
				}
			}
			
			// Return next
			return next;
		}
		
		// Get parent
		getParent() {
		
			// Create parent as a clone of self
			var parent = this.clone();
			
			// Check if previous depth exists
			if(this.getDepth() > 0) {
			
				// Clear parent's last path
				parent.paths[parent.getDepth() - 1] = 0;
				
				// Decrement parent's depth
				--parent.depth;
			}
			
			// Return parent
			return parent;
		}
		
		// Get child
		getChild(height = Identifier.NO_HEIGHT) {
		
			// Create child as a clone of self
			var child = this.clone();
			
			// Check if next depth exists
			if(this.getDepth() < Identifier.MAX_DEPTH) {
			
				// Increment child's depth
				++child.depth;
			}
			
			// Check if height exists
			if(height !== Identifier.NO_HEIGHT) {
			
				// Check wallet type
				switch(Consensus.getWalletType()) {
				
					// MWC wallet
					case Consensus.MWC_WALLET_TYPE:
			
						// Set child's height path as the height limited by its maximum value
						child.paths[Identifier.HEIGHT_PATH_INDEX] = Math.max(height.modulo(Identifier.MAXIMUM_HEIGHT + 1).toNumber(), 1);
						
						// Break
						break;
				}
			}
			
			// Return child
			return child;
		}
		
		// Get height
		getHeight() {
		
			// Check wallet type
			switch(Consensus.getWalletType()) {
			
				// MWC wallet
				case Consensus.MWC_WALLET_TYPE:
		
					// Get height from height path
					var height = this.getPaths()[Identifier.HEIGHT_PATH_INDEX];
					
					// Check if height exists
					if(height !== Consensus.FIRST_BLOCK_HEIGHT) {
					
						// Return height
						return new BigNumber(height);
					}
					
					// Otherwise
					else {
					
						// Return no height
						return Identifier.NO_HEIGHT;
					}
			}
			
			// Return no height
			return Identifier.NO_HEIGHT;
		}
		
		// Remove extras
		removeExtras() {
		
			// Create copy as a clone of self
			var copy = this.clone();
			
			// Check wallet type
			switch(Consensus.getWalletType()) {
			
				// MWC wallet
				case Consensus.MWC_WALLET_TYPE:
			
					// Set copy's height path to not exist
					copy.paths[Identifier.HEIGHT_PATH_INDEX] = Consensus.FIRST_BLOCK_HEIGHT;
					
					// Break
					break;
			}
			
			// Return copy
			return copy;
		}
		
		// Is path hardened
		static isPathHardened(path) {
		
			// Return if path is hardened
			return (path & Identifier.PATH_HARDENED_MASK) !== 0;
		}
		
		// Length
		static get LENGTH() {
		
			// Return length
			return 1 * Uint8Array["BYTES_PER_ELEMENT"] + Identifier.MAX_DEPTH * Uint32Array["BYTES_PER_ELEMENT"];
		}
		
		// Max depth
		static get MAX_DEPTH() {
		
			// Return max depth
			return 4;
		}
		
		// Root serialized identifier
		static get ROOT_SERIALIZED_IDENTIFIER() {
		
			// Return root serialized identifier
			return "0000000000000000000000000000000000";
		}
		
		// Paths index
		static get PATHS_INDEX() {
		
			// Return paths index
			return Identifier.DEPTH_INDEX + 1;
		}
		
		// No identifier
		static get NO_IDENTIFIER() {
		
			// Return no identifier
			return null;
		}
		
		// No heights
		static get NO_HEIGHT() {
		
			// Return no height
			return null;
		}
		
		// Maximum height
		static get MAXIMUM_HEIGHT() {
		
			// Return maximum height
			return Common.UINT32_MAX_VALUE;
		}
	
	// Private
	
		// Clone
		clone() {
		
			// Create a copy of self
			var copy = new Identifier();
			
			copy.setValue(this.getDepth(), this.getPaths());
			
			// Return copy
			return copy;
		}
		
		// Default serialized identifier
		static get DEFAULT_SERIALIZED_IDENTIFIER() {
		
			// Return default serialized identifier
			return "0200000000000000000000000000000000";
		}
		
		// Depth index
		static get DEPTH_INDEX() {
		
			// Return depth index
			return 0;
		}
		
		// Path hardened mask
		static get PATH_HARDENED_MASK() {
		
			// Return path hardened mask
			return 0x80000000;
		}
		
		// Height path index
		static get HEIGHT_PATH_INDEX() {
		
			// Return height path index
			return Identifier.MAX_DEPTH - 1;
		}
}


// Main function

// Set global object's identifier
globalThis["Identifier"] = Identifier;
