/******************************************************************************
 *                                                                            *
 *    This file is part of Kokopu, a JavaScript chess library.                *
 *    Copyright (C) 2018-2022  Yoann Le Montagner <yo35 -at- melix.net>       *
 *                                                                            *
 *    This program is free software: you can redistribute it and/or           *
 *    modify it under the terms of the GNU Lesser General Public License      *
 *    as published by the Free Software Foundation, either version 3 of       *
 *    the License, or (at your option) any later version.                     *
 *                                                                            *
 *    This program is distributed in the hope that it will be useful,         *
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of          *
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the            *
 *    GNU Lesser General Public License for more details.                     *
 *                                                                            *
 *    You should have received a copy of the GNU Lesser General               *
 *    Public License along with this program. If not, see                     *
 *    <http://www.gnu.org/licenses/>.                                         *
 *                                                                            *
 ******************************************************************************/


'use strict';


var bt = require('./basetypes');
var exception = require('./exception');
var i18n = require('./i18n');
var asciiImpl = require('./private_game/ascii_impl');

var Position = require('./position').Position;



// -----------------------------------------------------------------------------
// Game
// -----------------------------------------------------------------------------

/**
 * @class
 * @classdesc Chess game, with the move history, the position at each step of the game,
 *            the comments and annotations (if any), the result of the game,
 *            and some meta-data such as the name of the players, the date of the game,
 *            the name of the tournament, etc...
 */
var Game = exports.Game = function() {
	this._playerName  = [undefined, undefined];
	this._playerElo   = [undefined, undefined];
	this._playerTitle = [undefined, undefined];
	this._event     = undefined;
	this._round     = undefined;
	this._date      = undefined;
	this._site      = undefined;
	this._annotator = undefined;
	this._result    = bt.LINE;

	this._initialPosition = new Position();
	this._fullMoveNumber = 1;
	this._mainVariationInfo = createVariationInfo(this, true);
};


function sanitizeStringHeader(value) {
	return value === undefined || value === null ? undefined : String(value);
}


/**
 * Get the player name.
 *
 * @param {Color} color
 * @returns {string?}
 *
 *//**
 *
 * Set the player name.
 *
 * @param {Color} color
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.playerName = function(color, value) {
	color = bt.colorFromString(color);
	if(color < 0) { throw new exception.IllegalArgument('Game#playerName()'); }
	if(arguments.length === 1) { return this._playerName[color]; }
	else { this._playerName[color] = sanitizeStringHeader(value); }
};


/**
 * Get the player elo.
 *
 * @param {Color} color
 * @returns {string?}
 *
 *//**
 *
 * Set the player elo.
 *
 * @param {Color} color
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.playerElo = function(color, value) {
	color = bt.colorFromString(color);
	if(color < 0) { throw new exception.IllegalArgument('Game#playerElo()'); }
	if(arguments.length === 1) { return this._playerElo[color]; }
	else { this._playerElo[color] = sanitizeStringHeader(value); }
};


/**
 * Get the player title.
 *
 * @param {Color} color
 * @returns {string?}
 *
 *//**
 *
 * Set the player title.
 *
 * @param {Color} color
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.playerTitle = function(color, value) {
	color = bt.colorFromString(color);
	if(color < 0) { throw new exception.IllegalArgument('Game#playerTitle()'); }
	if(arguments.length === 1) { return this._playerTitle[color]; }
	else { this._playerTitle[color] = sanitizeStringHeader(value); }
};


/**
 * Get the event.
 *
 * @returns {string?}
 *
 *//**
 *
 * Set the event.
 *
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.event = function(value) {
	if(arguments.length === 0) { return this._event; }
	else { this._event = sanitizeStringHeader(value); }
};


/**
 * Get the round.
 *
 * @returns {string?}
 *
 *//**
 *
 * Set the round.
 *
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.round = function(value) {
	if(arguments.length === 0) { return this._round; }
	else { this._round = sanitizeStringHeader(value); }
};


/**
 * Get the date of the game.
 *
 * @returns {Date|{year:number, month:number}|{year:number}|undefined} Depending on what is defined, the method returns
 *          the whole date, or just the year and the month, or just the year, or `undefined`.
 *
 *//**
 *
 * Set the date of the game.
 *
 * @param {Date|{year:number, month:number}|{year:number}|undefined} value
 */
Game.prototype.date = function(value) {
	if(arguments.length === 0) {
		if (this._date instanceof Date) {
			return new Date(this._date);
		}
		else if (this._date) {
			return Object.assign({}, this._date);
		}
		else {
			return undefined;
		}
	}
	else if(value === undefined || value === null) {
		this._date = undefined;
	}
	else if(value instanceof Date) {
		this._date = new Date(value);
	}
	else if(typeof value === 'object' && typeof value.year === 'number' && typeof value.month === 'number' && value.month >= 1 && value.month <= 12) {
		this._date = { year: Math.round(value.year), month: Math.round(value.month) };
	}
	else if(typeof value === 'object' && typeof value.year === 'number' && (value.month === undefined || value.month === null)) {
		this._date = { year: Math.round(value.year) };
	}
	else {
		throw new exception.IllegalArgument('Game#date()');
	}
};


/**
 * Get the date of the game as a human-readable string (e.g. `'November 1955'`, `'September 4, 2021'`).
 *
 * @param {*?} locales Locales to use to generate the result. If undefined, the default locale of the execution environment is used.
 *                     See [Intl documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locale_identification_and_negotiation)
 *                     for more details.
 * @returns {string?}
 */
Game.prototype.dateAsString = function(locales) {
	if (!this._date) {
		return undefined;
	}
	if (this._date instanceof Date) {
		return new Intl.DateTimeFormat(locales, { dateStyle: 'long' }).format(this._date);
	}
	else if (this._date.month) {
		var firstDay = new Date(this._date.year, this._date.month - 1, 1);
		return new Intl.DateTimeFormat(locales, { month: 'long', year: 'numeric' }).format(firstDay);
	}
	else {
		return String(this._date.year);
	}
};


/**
 * Get where the game takes place.
 *
 * @returns {string?}
 *
 *//**
 *
 * Set where the game takes place.
 *
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.site = function(value) {
	if(arguments.length === 0) { return this._site; }
	else { this._site = sanitizeStringHeader(value); }
};


/**
 * Get the name of the annotator.
 *
 * @returns {string?}
 *
 *//**
 *
 * Set the name of the annotator.
 *
 * @param {*?} value If `null` or `undefined`, the existing value (if any) is erased.
 */
Game.prototype.annotator = function(value) {
	if(arguments.length === 0) { return this._annotator; }
	else { this._annotator = sanitizeStringHeader(value); }
};


/**
 * Get the result of the game.
 *
 * @returns {GameResult}
 *
 *//**
 *
 * Set the result of the game.
 *
 * @param {GameResult} value
 */
Game.prototype.result = function(value) {
	if(arguments.length === 0) {
		return bt.resultToString(this._result);
	}
	else {
		var result = bt.resultFromString(value);
		if(result < 0) {
			throw new exception.IllegalArgument('Game#result()');
		}
		this._result = result;
	}
};


/**
 * Get the {@link GameVariant} of the game.
 *
 * @returns {GameVariant}
 */
Game.prototype.variant = function() {
	return this._initialPosition.variant();
};


/**
 * Get the initial position of the game.
 *
 * @returns {Position}
 *
 *//**
 *
 * Set the initial position of the game.
 *
 * WARNING: this resets the main variation.
 *
 * @param {Position} initialPosition
 * @param {number} [fullMoveNumber=1]
 */
Game.prototype.initialPosition = function(initialPosition, fullMoveNumber) {
	if(arguments.length === 0) {
		return this._initialPosition;
	}
	else {
		if(!(initialPosition instanceof Position)) {
			throw new exception.IllegalArgument('Game#initialPosition()');
		}
		if(arguments.length === 1) {
			fullMoveNumber = 1;
		}
		else if(typeof fullMoveNumber !== 'number') {
			throw new exception.IllegalArgument('Game#initialPosition()');
		}
		this._initialPosition = initialPosition;
		this._fullMoveNumber = fullMoveNumber;
		this._mainVariationInfo = createVariationInfo(this, true);
	}
};


/**
 * The main variation of the game.
 *
 * @returns {Variation}
 */
Game.prototype.mainVariation = function() {
	return new Variation(this._mainVariationInfo, this._initialPosition);
};


/**
 * Return the node or variation corresponding to the given ID (see {@link Node#id} and {@link Variation#id}
 * to retrieve the ID of a node or variation).
 *
 * @return {(Node|Variation)?} `undefined` is returned if the given ID does not correspond to an existing {@link Node} and {@link Variation}.
 */
Game.prototype.findById = function(id) {
	var tokens = id.split('-');
	if (tokens.length % 2 !== 1) {
		return undefined;
	}
	var position = new Position(this._initialPosition);

	// Find the parent variation of the target node.
	var variationInfo = this._mainVariationInfo;
	for (var i = 0; i + 1 < tokens.length; i += 2) {
		var nodeInfo = findNode(variationInfo, tokens[i], position);
		if (!nodeInfo) {
			return undefined;
		}
		var match = /^v(\d+)$/.exec(tokens[i + 1]);
		if (!match) {
			return undefined;
		}
		var variationIndex = parseInt(match[1]);
		if (variationIndex >= nodeInfo.variations.length) {
			return undefined;
		}
		variationInfo = nodeInfo.variations[variationIndex];
	}

	// Find the target node within its parent variation, or return the variation itself
	// if the ID is a variation ID (i.e. if it ends with 'start').
	var lastToken = tokens[tokens.length - 1];
	if (lastToken === 'start') {
		return new Variation(variationInfo, position);
	}
	else {
		var nodeInfo = findNode(variationInfo, lastToken, position);
		return nodeInfo ? new Node(nodeInfo, position) : undefined;
	}
};


function findNode(variationInfo, nodeIdToken, position) {
	var nodeInfo = variationInfo.child;
	while (nodeInfo) {
		if (nodeIdToken === nodeInfo.fullMoveNumber + nodeInfo.moveColor) {
			return nodeInfo;
		}
		applyMoveDescriptor(position, nodeInfo);
		nodeInfo = nodeInfo.child;
	}
	return undefined;
}


/**
 * Return the {@link Node}-s corresponding to the moves of the main variation.
 *
 * @param {boolean} [withSubVariations=false] If `true`, the nodes of the sub-variations are also included in the result.
 * @returns {Node[]} An empty array is returned if the main variation is empty.
 */
Game.prototype.nodes = function(withSubVariations) {
	if (!withSubVariations) {
		return this.mainVariation().nodes();
	}

	var result = [];
	function processVariation(variation) {
		var currentNodes = variation.nodes();
		for (var i = 0; i < currentNodes.length; ++i) {
			var nextVariations = currentNodes[i].variations();
			for (var j = 0; j < nextVariations.length; ++j) {
				processVariation(nextVariations[j]);
			}
			result.push(currentNodes[i]);
		}
	}
	processVariation(this.mainVariation());
	return result;
};


/**
 * Return a human-readable string representing the game. This string is multi-line,
 * and is intended to be displayed in a fixed-width font (similarly to an ASCII-art picture).
 *
 * @returns {string}
 */
Game.prototype.ascii = function() {
	return asciiImpl.ascii(this);
};



// -----------------------------------------------------------------------------
// Node
// -----------------------------------------------------------------------------

/**
 * @param {object} parentVariation VariantInfo struct
 * @param {Color} moveColor
 * @param {number} fullMoveNumber
 * @param {MoveDescriptor} moveDescriptor
 * @returns {object}
 * @ignore
 */
function createNodeInfo(parentVariation, moveColor, fullMoveNumber, moveDescriptor) {
	return {
		parentVariation: parentVariation,

		// Attributes of the current move.
		moveColor: moveColor,
		fullMoveNumber: fullMoveNumber,
		moveDescriptor: moveDescriptor, // `moveDescriptor` is `undefined` in case of a null-move.

		// Next move and alternative variations.
		child: undefined,
		variations: [],

		// Annotations and comments associated to the underlying move.
		nags: {},
		tags: {},
		comment: undefined,
		isLongComment: false
	};
}


/**
 * @class
 * @classdesc Represent one move in the tree structure formed by a chess game with multiple variations.
 *
 * @description This constructor is not exposed in the public Kokopu API. Only internal objects and functions
 *              are allowed to instantiate {@link Node} objects.
 */
function Node(info, positionBefore) {
	this._info = info;
	this._positionBefore = positionBefore;
}


/**
 * Play the move descriptor encoded in the given node info structure, or play null-move if no move descriptor is defined.
 *
 * @param {Position} position
 * @param {object} info
 * @ignore
 */
function applyMoveDescriptor(position, info) {
	if(info.moveDescriptor === undefined) {
		position.playNullMove();
	}
	else {
		position.play(info.moveDescriptor);
	}
}


/**
 * Identifier of the current node within its parent {@link Game}.
 *
 * WARNING: the ID may change when variations are modified (added, removed, swapped, promoted...)
 * among the parents the current node.
 *
 * @returns {string}
 */
Node.prototype.id = function() {
	return buildNodeId(this._info);
};


/**
 * Always `false`. Useful to discrimate between {@link Node} and {@link Variation} instances.
 *
 * @returns {boolean}
 */
Node.prototype.isVariation = function() {
	return false;
};


/**
 * Compute the ID of the given node.
 *
 * @param {object} nodeInfo NodeInfo struct
 * @returns {string}
 * @ignore
 */
function buildNodeId(nodeInfo) {
	return buildVariationIdPrefix(nodeInfo.parentVariation) + nodeInfo.fullMoveNumber + nodeInfo.moveColor;
}


/**
 * Return the {@link Variation} that owns the current node.
 *
 * @returns {Variation}
 */
Node.prototype.parentVariation = function() {
	var position = rebuildVariationPosition(this._info.parentVariation);
	return new Variation(this._info.parentVariation, position);
};


/**
 * Return the {@link Node} that comes before the current one in their parent variation.
 *
 * @returns {Node?} `undefined` if the current node is the first one of the variation.
 */
Node.prototype.previous = function() {
	var position = rebuildVariationPosition(this._info.parentVariation);
	var current = this._info.parentVariation.child;
	if (current === this._info) {
		return undefined;
	}
	while (current.child !== this._info) {
		applyMoveDescriptor(position, current);
		current = current.child;
	}
	return new Node(current, position);
};


/**
 * Return the initial position of the given variation.
 *
 * @param {object} variationInfo VariationInfo
 * @returns {Position}
 * @ignore
 */
function rebuildVariationPosition(variationInfo) {
	if (variationInfo.parentNode instanceof Game) {
		return new Position(variationInfo.parentNode._initialPosition);
	}
	else {
		var position = rebuildVariationPosition(variationInfo.parentNode.parentVariation);
		var current = variationInfo.parentNode.parentVariation.child;
		while (current !== variationInfo.parentNode) {
			applyMoveDescriptor(position, current);
			current = current.child;
		}
		return position;
	}
}


/**
 * SAN representation of the move associated to the current node (or `'--'` for a null-move).
 *
 * @returns {string}
 */
Node.prototype.notation = function() {
	return this._info.moveDescriptor === undefined ? '--' : this._positionBefore.notation(this._info.moveDescriptor);
};


/**
 * SAN-like representation of the move associated to the current node (or `'--'` for a null-move).
 *
 * @returns {string} Chess pieces are represented with their respective unicode character, instead of the first letter of their English name.
 */
Node.prototype.figurineNotation = function() {
	return this._info.moveDescriptor === undefined ? '--' : this._positionBefore.figurineNotation(this._info.moveDescriptor);
};


/**
 * Chess position before the current move.
 *
 * @returns {Position}
 */
Node.prototype.positionBefore = function() {
	return new Position(this._positionBefore);
};


/**
 * Chess position obtained after the current move.
 *
 * @returns {Position}
 */
Node.prototype.position = function() {
	var position = this.positionBefore();
	applyMoveDescriptor(position, this._info);
	return position;
};


/**
 * Full-move number. It starts at 1, and is incremented after each black move.
 *
 * @returns {number}
 */
Node.prototype.fullMoveNumber = function() {
	return this._info.fullMoveNumber;
};


/**
 * Color the side corresponding to the current move.
 *
 * @returns {Color}
 */
Node.prototype.moveColor = function() {
	return this._info.moveColor;
};


/**
 * Return the {@link Node} that comes after the current one in their parent variation.
 *
 * @returns {Node?} `undefined` if the current node is the last one of the variation.
 */
Node.prototype.next = function() {
	if (!this._info.child) {
		return undefined;
	}
	var nextPositionBefore = new Position(this._positionBefore);
	applyMoveDescriptor(nextPositionBefore, this._info);
	return new Node(this._info.child, nextPositionBefore);
};


/**
 * Return the variations that can be followed instead of the current move.
 *
 * @returns {Variation[]}
 */
Node.prototype.variations = function() {
	if(this._info.variations.length === 0) {
		return [];
	}

	var result = [];
	for(var i = 0; i < this._info.variations.length; ++i) {
		result.push(new Variation(this._info.variations[i], this._positionBefore));
	}
	return result;
};


function isValidNag(nag) {
	return typeof nag === 'number' && !isNaN(nag) && nag >= 0;
}


/**
 * Return the NAGs associated to the current move.
 *
 * @returns {number[]} Sorted array.
 */
Node.prototype.nags = function() {
	var result = [];
	for(var key in this._info.nags) {
		result.push(Number(key));
	}
	return result.sort(function(a, b) { return a - b; });
};


/**
 * Check whether the current move has the given NAG or not.
 *
 * @param {number} nag
 * @returns {boolean}
 */
Node.prototype.hasNag = function(nag) {
	if (!isValidNag(nag)) {
		throw new exception.IllegalArgument('Node#hasNag()');
	}
	return Boolean(this._info.nags[nag]);
};


/**
 * Add the given NAG to the current move.
 *
 * @param {number} nag
 */
Node.prototype.addNag = function(nag) {
	if (!isValidNag(nag)) {
		throw new exception.IllegalArgument('Node#addNag()');
	}
	this._info.nags[nag] = true;
};


/**
 * Remove the given NAG from the current move.
 *
 * @param {number} nag
 */
Node.prototype.removeNag = function(nag) {
	if (!isValidNag(nag)) {
		throw new exception.IllegalArgument('Node#removeNag()');
	}
	delete this._info.nags[nag];
};


/**
 * Return the keys of the tags associated to the current move.
 *
 * @returns {string[]} Sorted array.
 */
Node.prototype.tags = function() {
	var result = [];
	for(var key in this._info.tags) {
		result.push(key);
	}
	return result.sort();
};


/**
 * Get the value associated to the given tag key on the current move.
 *
 * @param {string} tagKey
 * @returns {string?} `undefined` if no value is associated to this tag key on the current move.
 *
 *//**
 *
 * Set the value associated to the given tag key on the current move.
 *
 * @param {string} tagKey
 * @param {string?} value
 */
Node.prototype.tag = function(tagKey, value) {
	if (!/^\w+$/.test(tagKey)) {
		throw new exception.IllegalArgument('Node#tag()');
	}
	if (arguments.length === 1) {
		return this._info.tags[tagKey];
	}
	else if (value === undefined || value === null) {
		delete this._info.tags[tagKey];
	}
	else {
		this._info.tags[tagKey] = String(value);
	}
};


/**
 * Get the text comment associated to the current move.
 *
 * @returns {string?} `undefined` if no comment is defined for the move.
 *
 *//**
 *
 * Set the text comment associated to the current move.
 *
 * @param {string} value
 * @param {boolean} [isLongComment=false]
 */
Node.prototype.comment = function(value, isLongComment) {
	if(arguments.length === 0) {
		return this._info.comment;
	}
	else {
		this._info.comment = sanitizeStringHeader(value);
		this._info.isLongComment = this._info.comment && isLongComment;
	}
};


/**
 * Whether the text comment associated to the current move is long or short.
 *
 * @returns {boolean} Always `false` if no comment is defined.
 */
Node.prototype.isLongComment = function() {
	return this._info.isLongComment && doIsLongVariation(this._info.parentVariation);
};


/**
 * Compute the move descriptor associated to the given SAN notation, assuming the given position.
 *
 * @param {Position} position Position based on which the given SAN notation must be interpreted.
 * @param {string} move SAN notation (or `'--'` for a null-move).
 * @returns {MoveDescriptor?} `undefined` is returned in case of a null-move.
 * @throws {module:exception.InvalidNotation} If the move notation cannot be parsed.
 * @ignore
 */
function computeMoveDescriptor(position, move) {
	if(move === '--') {
		if(!position.isNullMoveLegal()) {
			throw new exception.InvalidNotation(position, '--', i18n.ILLEGAL_NULL_MOVE);
		}
		return undefined;
	}
	else {
		return position.notation(move);
	}
}


/**
 * Play the given move, and return a new {@link Node} pointing at the resulting position.
 *
 * @param {string} move SAN notation (or `'--'` for a null-move).
 * @returns {Node} A new node, pointing at the new position.
 * @throws {module:exception.InvalidNotation} If the move notation cannot be parsed.
 */
Node.prototype.play = function(move) {
	var nextPositionBefore = new Position(this._positionBefore);
	applyMoveDescriptor(nextPositionBefore, this._info);
	var nextMoveColor = nextPositionBefore.turn();
	var nextFullMoveNumber = nextMoveColor === 'w' ? this._info.fullMoveNumber + 1: this._info.fullMoveNumber;
	this._info.child = createNodeInfo(this._info.parentVariation, nextMoveColor, nextFullMoveNumber, computeMoveDescriptor(nextPositionBefore, move));
	return new Node(this._info.child, nextPositionBefore);
};


/**
 * Erase all the moves after the one on the current {@link Node}: after that, {@link Node#next} returns `undefined`.
 * If the current {@link Node} is already the last one in its variation (i.e. if {@link Node#next} returns `undefined` already),
 * nothing happens.
 */
Node.prototype.removeFollowingMoves = function() {
	this._info.child = undefined;
};


/**
 * Create a new variation that can be played instead of the current move.
 *
 * @param {boolean} isLongVariation
 * @returns {Variation}
 */
Node.prototype.addVariation = function(isLongVariation) {
	this._info.variations.push(createVariationInfo(this._info, isLongVariation));
	return new Variation(this._info.variations[this._info.variations.length - 1], this._positionBefore);
};


/**
 * Remove the variation corresponding to the given index.
 *
 * @param {number} variationIndex Index of the variation to promote (must be such that `0 <= variationIndex < thisNode.variations().length`).
 */
Node.prototype.removeVariation = function(variationIndex) {
	if (!this._info.variations[variationIndex]) {
		throw new exception.IllegalArgument('Node#removeVariation()');
	}
	this._info.variations = this._info.variations.slice(0, variationIndex).concat(this._info.variations.slice(variationIndex + 1));
};


/**
 * Change the order of the variations by swapping the two variations corresponding to the given indexes.
 *
 * @param {number} variationIndex1 Index of one variation to swap (must be such that `0 <= variationIndex1 < thisNode.variations().length`).
 * @param {number} variationIndex2 Index of the other variation to swap (must be such that `0 <= variationIndex2 < thisNode.variations().length`).
 */
Node.prototype.swapVariations = function(variationIndex1, variationIndex2) {
	var variation1 = this._info.variations[variationIndex1];
	var variation2 = this._info.variations[variationIndex2];
	if (!variation1 || !variation2) {
		throw new exception.IllegalArgument('Node#swapVariations()');
	}
	this._info.variations[variationIndex1] = variation2;
	this._info.variations[variationIndex2] = variation1;
};


/**
 * Replace the move on the current node (and the following ones, if any) by the moves of the variation corresponding to the given index,
 * and create a new variation with the move on the current node and its successors.
 *
 * WARNING: the promoted variation must NOT be empty.
 *
 * @param {number} variationIndex Index of the variation to promote (must be such that `0 <= variationIndex < thisNode.variations().length`).
 *                                If the corresponding variation is empty, an exception is thrown.
 */
Node.prototype.promoteVariation = function(variationIndex) {
	var variationToPromote = this._info.variations[variationIndex];
	if (!variationToPromote || !variationToPromote.child) {
		throw new exception.IllegalArgument('Node#promoteVariation()');
	}
	var oldMainLine = this._info;
	var newMainLine = variationToPromote.child;

	// Detach the array containing the variations from the current node.
	var variations = oldMainLine.variations;
	oldMainLine.variations = [];

	// Create a new variation with the old main line.
	variations[variationIndex] = createVariationInfo(newMainLine, false);
	variations[variationIndex].child = oldMainLine;

	// Create a new main line with the promoted variation, and re-attach the variations.
	this._info = newMainLine;
	newMainLine.variations = variations.concat(newMainLine.variations);

	// Re-map the parents.
	var parent = findParent(oldMainLine);
	parent.child = newMainLine;
	resetParentVariationRecursively(newMainLine, oldMainLine.parentVariation);
	resetParentVariationRecursively(oldMainLine, variations[variationIndex]);
	for (var variationIndex = 0; variationIndex < newMainLine.variations.length; ++variationIndex) {
		newMainLine.variations[variationIndex].parentNode = newMainLine;
	}
};


function findParent(oldMainLine) {
	var candidate = oldMainLine.parentVariation;
	while (candidate.child !== oldMainLine) {
		candidate = candidate.child;
	}
	return candidate;
}


function resetParentVariationRecursively(root, newParentVariation) {
	while (root) {
		root.parentVariation = newParentVariation;
		root = root.child;
	}
}


// -----------------------------------------------------------------------------
// Variation
// -----------------------------------------------------------------------------

/**
 * @param {object|Game} parentNode NodeInfo struct (or `Game` for the main variation)
 * @param {boolean} isLongVariation
 * @returns {object}
 * @ignore
 */
function createVariationInfo(parentNode, isLongVariation) {
	return {
		parentNode: parentNode,
		isLongVariation: isLongVariation,

		// First move of the variation.
		child: undefined,

		// Annotations and comments associated to the underlying variation.
		nags: {},
		tags: {},
		comment: undefined,
		isLongComment: false
	};
}


/**
 * @class
 * @classdesc Represent one variation in the tree structure formed by a chess game, meaning
 * a starting chess position and list of played consecutively from this position.
 *
 * @description This constructor is not exposed in the public Kokopu API. Only internal objects and functions
 *              are allowed to instantiate {@link Variation} objects.
 */
function Variation(info, initialPosition) {
	this._info = info;
	this._initialPosition = initialPosition;
}


/**
 * Identifier of the current variation within its parent {@link Game}.
 *
 * WARNING: the ID may change when variations are modified (added, removed, swapped, promoted...)
 * among the parents the current variation.
 *
 * @returns {string}
 */
Variation.prototype.id = function() {
	return buildVariationIdPrefix(this._info) + 'start';
};


/**
 * Always `true`. Useful to discrimate between {@link Node} and {@link Variation} instances.
 *
 * @returns {boolean}
 */
Variation.prototype.isVariation = function() {
	return true;
};


/**
 * Compute the ID of the given variation, without the final `'start'` token.
 *
 * @param {object} variationInfo VariationInfo struct
 * @returns {string}
 * @ignore
 */
function buildVariationIdPrefix(variationInfo) {
	if (variationInfo.parentNode instanceof Game) {
		return '';
	}
	else {
		var parentNodeId = buildNodeId(variationInfo.parentNode);
		var variationIndex = variationInfo.parentNode.variations.indexOf(variationInfo);
		return parentNodeId + '-v' + variationIndex + '-';
	}
}


/**
 * Return the {@link Node} to which the current variation is attached.
 *
 * @returns {Node?} `undefined` if the current variation is the main one (see {@link Game#mainVariation}).
 */
Variation.prototype.parentNode = function() {
	return this._info.parentNode instanceof Game ? undefined : new Node(this._info.parentNode, this._initialPosition);
};


/**
 * Whether the current variation is considered as a "long" variation, i.e. a variation that
 * should be displayed in an isolated block.
 *
 * @returns {boolean}
 */
Variation.prototype.isLongVariation = function() {
	return doIsLongVariation(this._info);
};


/**
 * Whether the variation corresponding to the given descriptor is a "long variation",
 * i.e. whether it is a flagged as "isLongVariation" AND SO ARE ALL IT'S PARENTS.
 *
 * @param {object} variationInfo VariationInfo struct
 * @returns {boolean}
 * @ignore
 */
function doIsLongVariation(variationInfo) {
	while (true) {
		if (!variationInfo.isLongVariation) {
			return false;
		}
		if (variationInfo.parentNode instanceof Game) {
			return true;
		}
		variationInfo = variationInfo.parentNode.parentVariation;
	}
}


/**
 * Chess position at the beginning of the variation.
 *
 * @returns {Position}
 */
Variation.prototype.initialPosition = function() {
	return new Position(this._initialPosition);
};


/**
 * Full-move number at the beginning of the variation.
 *
 * @returns {number}
 */
Variation.prototype.initialFullMoveNumber = function() {
	return this._info.parentNode instanceof Game ? this._info.parentNode._fullMoveNumber : this._info.parentNode.fullMoveNumber;
};


/**
 * First move of the variation.
 *
 * @returns {Node?} `undefined` if the variation is empty.
 */
Variation.prototype.first = function() {
	if (!this._info.child) {
		return undefined;
	}
	return new Node(this._info.child, this._initialPosition);
};


/**
 * Return the {@link Node}-s corresponding to the moves of the current variation.
 *
 * @returns {Node[]} An empty array is returned if the variation is empty.
 */
Variation.prototype.nodes = function() {
	var result = [];

	var currentNodeInfo = this._info.child;
	var previousNodeInfo = undefined;
	var previousPositionBefore = this._initialPosition;
	while (currentNodeInfo) {

		// Compute the "position-before" attribute the current node.
		previousPositionBefore = new Position(previousPositionBefore);
		if (previousNodeInfo) {
			applyMoveDescriptor(previousPositionBefore, previousNodeInfo);
		}

		// Push the current node.
		result.push(new Node(currentNodeInfo, previousPositionBefore));

		// Increment the counters.
		previousNodeInfo = currentNodeInfo;
		currentNodeInfo = currentNodeInfo.child;
	}

	return result;
};


/**
 * Return the NAGs associated to the current variation.
 *
 * @returns {number[]} Sorted array.
 * @function
 */
Variation.prototype.nags = Node.prototype.nags;


/**
 * Check whether the current variation has the given NAG or not.
 *
 * @param {number} nag
 * @returns {boolean}
 * @function
 */
Variation.prototype.hasNag = Node.prototype.hasNag;


/**
 * Add the given NAG to the current variation.
 *
 * @param {number} nag
 * @function
 */
Variation.prototype.addNag = Node.prototype.addNag;


/**
 * Remove the given NAG from the current variation.
 *
 * @param {number} nag
 * @function
 */
Variation.prototype.removeNag = Node.prototype.removeNag;


/**
 * Return the keys of the tags associated to the current variation.
 *
 * @returns {string[]} Sorted array.
 * @function
 */
Variation.prototype.tags = Node.prototype.tags;


/**
 * Get the value associated to the given tag key on the current variation.
 *
 * @param {string} tagKey
 * @returns {string?} `undefined` if no value is associated to this tag key on the current variation.
 * @function
 *
 *//**
 *
 * Set the value associated to the given tag key on the current variation.
 *
 * @param {string} tagKey
 * @param {string?} value
 * @function
 */
Variation.prototype.tag = Node.prototype.tag;


/**
 * Get the text comment associated to the current variation.
 *
 * @returns {string?} `undefined` if no comment is defined for the variation.
 * @function
 *
 *//**
 *
 * Set the text comment associated to the current variation.
 *
 * @param {string} value
 * @param {boolean} [isLongComment=false]
 * @function
 */
Variation.prototype.comment = Node.prototype.comment;


/**
 * Whether the text comment associated to the current variation is long or short.
 *
 * @returns {boolean}
 */
Variation.prototype.isLongComment = function() {
	return this._info.isLongComment && this.isLongVariation();
};


/**
 * Play the given move as the first move of the variation.
 *
 * @param {string} move SAN notation (or `'--'` for a null-move).
 * @returns {Node} A new node object, to represents the new move.
 * @throws {module:exception.InvalidNotation} If the move notation cannot be parsed.
 */
Variation.prototype.play = function(move) {
	this._info.child = createNodeInfo(this._info, this._initialPosition.turn(), this.initialFullMoveNumber(), computeMoveDescriptor(this._initialPosition, move));
	return new Node(this._info.child, this._initialPosition);
};


/**
 * Erase all the moves in the current {@link Variation}: after that, {@link Variation#first} returns `undefined`.
 * If the current {@link Variation} is already empty (i.e. if {@link Variation#first} returns `undefined` already),
 * nothing happens.
 */
Variation.prototype.clearMoves = function() {
	this._info.child = undefined;
};