/*jslint node: true, vars: true, nomen: true */
'use strict';

var async = require('async');
var fs = require('fs');
var util = require('util');
var crc32 = require('crc').crc32;
var debug = require('debug')('matroska:document');

var schema = require('./schema');
var Element = require('./element');
var tools = require('./tools');

// we have to require all of them to be browserify/webpack compatible
// find dom | grep ".js" | grep -v _proto | while read line; do echo "require('./$line');"; done
require('./dom/attachedFile.js');
require('./dom/attachments.js');
require('./dom/audio.js');
require('./dom/crc-32.js');
require('./dom/cuePoint.js');
require('./dom/cueReference.js');
require('./dom/cues.js');
require('./dom/cueTrackPositions.js');
require('./dom/element1.js');
require('./dom/info.js');
require('./dom/masterElement.js');
require('./dom/seek.js');
require('./dom/seekHead.js');
require('./dom/segment.js');
require('./dom/segment1.js');
require('./dom/segment2.js');
require('./dom/segment3.js');
require('./dom/simpleTag.js');
require('./dom/tag.js');
require('./dom/tags.js');
require('./dom/targets.js');
require('./dom/trackEntry.js');
require('./dom/tracks.js');
require('./dom/video.js');

var tagClasses = {};
tagClasses[schema.byName.Segment] = "segment3";
tagClasses[schema.byName.Attachments] = "attachments";
tagClasses[schema.byName.AttachedFile] = "attachedFile";
tagClasses[schema.byName.Tags] = "tags";
tagClasses[schema.byName.Tag] = "tag";
tagClasses[schema.byName.Targets] = "targets";
tagClasses[schema.byName.SimpleTag] = "simpleTag";
tagClasses[schema.byName.SeekHead] = "seekHead";
tagClasses[schema.byName.Seek] = "seek";
tagClasses[schema.byName.Info] = "info";
tagClasses[schema.byName.Tracks] = "tracks";
tagClasses[schema.byName.TrackEntry] = "trackEntry";
tagClasses[schema.byName.Video] = "video";
tagClasses[schema.byName.Audio] = "audio";
tagClasses[schema.byName.CRC_32] = "crc-32";
tagClasses[schema.byName.Cues] = "cues";
tagClasses[schema.byName.CuePoint] = "cuePoint";
tagClasses[schema.byName.CueReference] = "cueReference";
tagClasses[schema.byName.CueTrackPositions] = "cueTrackPositions";

/**
 * @constructs
 * @private
 */
function Document() {
	this.type = "D";
	this._name = "Document";
	this.tagId = 0;
	this._nextTagId = 1;
	this.ownerDocument = this;
	this.masterType = true;
}

util.inherits(Document, Element);

module.exports = Document;

Document.prototype.createElement = function(ebmlID, start, length) {
	var element;

	var tagClass = tagClasses[ebmlID];
	if (tagClass) {
		if (typeof (tagClass) === "string") {
			tagClass = require('./dom/' + tagClass);
			tagClasses[ebmlID] = tagClass;
		}

		element = new tagClass(this, this._nextTagId++, start, length);

	} else {
		element = new Element(this, this._nextTagId++, ebmlID, start, length);
	}

	return element;
};

/**
 * @private
 */
Document.prototype._registerPosition = function(tag) {
	if (!this._positions) {
		this._positions = [];
	}

	this._positions.push(tag);

	if (this._modified) {
		tag._markModified();
	}
};

function removeList(list, tag) {
	if (!list || !list.length) {
		return false;
	}
	var idx = list.indexOf(tag);
	if (idx < 0) {
		return false;
	}

	list.splice(idx, 1);
	return true;
}

/**
 * @private
 */
Document.prototype._unregisterPosition = function(tag) {
	return removeList(this._positions, tag);
};

/**
 * @private
 */
Document.prototype._registerCRC = function(tag) {
	if (!this._crcs) {
		this._crcs = [];
	}

	// console.log("Register tag #" + tag.tagId);

	this._crcs.push(tag);
};

/**
 * @private
 */
Document.prototype._unregisterCRC = function(tag) {
	return removeList(this._crcs, tag);
};

/**
 * @private
 */
Document.prototype._markModified = function() {
	if (this._modified) {
		return;
	}

	if (this._partial) {
		throw new Error("Can not modify a partial parsed document");
	}

	this._modified = true;

	if (!this._positions) {
		return;
	}

	this._positions.forEach(function(child) {
		child._markModified();
	});
};

/**
 * @private
 */
Document.prototype._buildLinks = function() {
	if (this._linksBuilt) {
		return;
	}

	this._linksBuilt = true;

	if (!this._positions) {
		return;
	}

	var self = this;

	this._positions.forEach(function(child) {
		var offset = child.getValue();

		var parent1 = child.getLevel1();
		var targetType = child._positionTargetType;

		switch (targetType) {
		case "segment":
			break;

		case "clusterRelative":
			var cp = child.parent.cueClusterPosition;
			if (!cp) {
				console.log("parent=", child.parent);
				throw new Error("Invalid cluster relative without a cueClusterPosition");
			}
			offset += cp._positionTarget ? cp._positionTarget.getContentPosition() : cp; // this assumed _positionTarget, but usually cp is simply an integer
			break;

		case "cluster":

		default:
			throw new Error("Not supported ! (" + targetType + ")");
		}

		var target = parent1.getTagByPosition(offset, true);

		if (debug.enabled) {
			debug("Position #" + parent1.tagId + " " + offset + "=> " + (target ? ("#" + target.tagID) : "null"));
		}

		if (!target || target.position !== "start") {
			// In partial mode, some nodes might not be loaded !
			if (!self._partial) {
				debug("Can not find target for offset=" + offset + " doc=" + self);
			}
			return;
		}

		child._positionTarget = target.target;
	});

};

/**
 * Write MKV file
 * 
 * @param {(Stream|string)}
 *          stream Stream or path of the destination file
 * @param {Object}
 *          [options] Options object
 * @param {function}
 *          callback Called when done
 */
Document.prototype.write = function(stream, options, callback) {

	if (typeof (options) === "function" && arguments.length === 2) {
		callback = options;
		options = null;
	}

	options = options || {};

	var self = this;

	if (this._partial) {
		return callback(new Error("The document is not complete"))
	}

	this._prepareDocument(options, function(error) {
		if (error) {
			return callback(error);
		}

		// console.log("Computing positions ...");
		self._computePositions(function(error) {
			if (error) {
				return callback(error);
			}

			var source = self.source;

			self._updateCRC32(function(error) {
				if (error) {
					return callback(error);
				}

				var closeStream = false;
				if (typeof (stream) === "string") {
					console.log("Write to file ", stream);
					stream = fs.createWriteStream(stream);
					closeStream = true;
				}

				var writeSession = {
					stream: stream,
					options: options
				};

				self._write(writeSession, source, function(error) {
					if (error) {
						return callback(error);
					}

					source.end(writeSession, function(error) {
						if (error) {
							return callback(error);
						}

						if (!closeStream) {
							return callback();
						}

						// console.error("Close stream");

						stream.end(callback);
					});
				});
			});
		});
	});
};

/**
 * @private
 */
Document.prototype._prepareDocument = function(options, callback) {
	return callback();
};

/**
 * @private
 */
Document.prototype._write = function(output, source, callback) {
	async.eachSeries(this.children, function(child, callback) {
		child._write(output, source, callback);

	}, callback);
};

/**
 * @private
 */
Document.prototype._computePositions = function(estimatedFileSize, callback) {

	if (!this._positions || !this._positions.length) {
		return callback();
	}

	if (typeof (estimatedFileSize) === 'function') {
		callback = estimatedFileSize;
		estimatedFileSize = null;
	}

	if (!estimatedFileSize) {
		estimatedFileSize = 0;

		this.children.forEach(function(child) {
			var s = child._getSize();
			estimatedFileSize += s;
		});

	}

	var bits = tools.sizeUInt(estimatedFileSize) * 8;
	var max = Math.pow(2, bits) - 1;

	// console.log("Estimated size=" + estimatedFileSize + " max=" + max + "
	// bits=" + bits);

	var error = false;
	this._positions.forEach(function(child) {
		child.setUInt(max);

		var target = child._positionTarget;
		if (!target) {
			console.error("No target for tag '" + child._name + "' #" + child.tagId);
			error = true;
			return;
		}

		if (false) {
			console.error("Target has been removed target=" + target._name + "#" + target.tagId);
			error = true;
			return;
		}

		var level1s = child.getLevel1();
		var level1t = target.getLevel1();

		if (level1s !== level1t) {
			console.error("Target and source have not the same Level1 target=" + target._name + "#" + target.tagId);
			error = true;
			return;
		}

	});
	if (error) {
		return callback(error);
	}

	var tx = 1;
	var times = 5;
	var changes = 1;
	for (; changes && times; times--, tx++) {
		changes = 0;

		this._positions.forEach(function(child) {
			var target = child._positionTarget;
			var level1 = target.getLevel1();

			var newPosition = target.getPosition() - level1.getContentPosition();
			var oldPosition = child.getUInt();

			if (newPosition === oldPosition) {
				return;
			}

			// console.log("Change position old=" + child.getUInt() + " new=" +
			// newPosition);

			child.setUInt(newPosition);
			changes++;
		});

		console.log("Position pass #" + tx + ": " + changes + " modified positions");
	}

	return callback();
};

Document.prototype._updateCRC32 = function(callback) {
	var crcs = this._crcs;

	if (!crcs || !crcs.length) {
		console.log("No crc to compute ", crcs);
		return callback();
	}

	console.log(crcs.length + " crc to compute ...");

	function depth(t) {
		var ret = 0;
		for (; t; t = t.parent) {
			ret++;
		}

		return ret;
	}

	crcs.sort(function(t1, t2) {
		return depth(t2) - depth(t1);
	});

	async.eachSeries(crcs, function(crcTag, callback) {
		var parent = crcTag.parent;

		// console.log("Compute crc #" + crcTag.tagId + " parent #" + parent.tagId);

		if (false && !parent._modified) {
			// console.log("Compute crc #" + parent.tagId + " => not modified");
			return callback();
		}

		parent._computeChildrenCRC(true, null, function(error, crc) {
			if (error) {
				return callback(error);
			}

			if (crcTag.data) {
				var oldCRC = crcTag.getCRCValue();
				if (crc == oldCRC) {
					// console.log("No change keep old CRC #" + crcTag.tagId + " 0x",
					// crc.toString(16));
					return callback();
				}
				// console.log("Change CRC #" + crcTag.tagId + " new=", crc, " old=",
				// oldCRC);
			}

			crcTag.setCRCValue(crc);
			callback();
		});

	}, callback);
};

/**
 * Get tag by its tag identifier (number)
 * 
 * @param {number}
 *          tagId Tag identifier
 * @returns {Tag} The found tag or null
 */
Document.prototype.getTagById = function(tagId) {
	if (isNaN(tagId)) {
		throw new Error("Tag identifier is not supported (" + tagId + ")");
	}

	var ret = this.deepWalk(function(child) {
		if (child.tagId === tagId) {
			return child;
		}
	});

	return ret;
};

Document.prototype.toString = function() {
	return "[Document source=" + this.source + "]";
}
