/*!
 * @author Neil Kandalgaonkar, 2010
 * @since 1.18
 */

/* Private members */

let toUpperMap;

const mwString = require( 'mediawiki.String' ),

	namespaceIds = mw.config.get( 'wgNamespaceIds' ),

	/**
	 * @private
	 * @static
	 * @property {number} NS_MAIN
	 */
	NS_MAIN = namespaceIds[ '' ],

	/**
	 * @private
	 * @static
	 * @property {number} NS_TALK
	 */
	NS_TALK = namespaceIds.talk,

	/**
	 * @private
	 * @static
	 * @property {number} NS_SPECIAL
	 */
	NS_SPECIAL = namespaceIds.special,

	/**
	 * @private
	 * @static
	 * @property {number} NS_MEDIA
	 */
	NS_MEDIA = namespaceIds.media,

	/**
	 * @private
	 * @static
	 * @property {number} NS_FILE
	 */
	NS_FILE = namespaceIds.file,

	/**
	 * @private
	 * @static
	 * @property {number} FILENAME_MAX_BYTES
	 */
	FILENAME_MAX_BYTES = 240,

	/**
	 * @private
	 * @static
	 * @property {number} TITLE_MAX_BYTES
	 */
	TITLE_MAX_BYTES = 255,

	/**
	 * Get the namespace id from a namespace name (either from the localized, canonical or alias
	 * name).
	 *
	 * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or
	 * even 'Bild'.
	 *
	 * @private
	 * @static
	 * @method getNsIdByName
	 * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored)
	 * @return {number|boolean} Namespace id or boolean false
	 */
	getNsIdByName = function ( ns ) {
		// Don't cast non-strings to strings, because null or undefined should not result in
		// returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki)
		// Also, toLowerCase throws exception on null/undefined, because it is a String method.
		if ( typeof ns !== 'string' ) {
			return false;
		}
		// TODO: Should just use the local variable namespaceIds here, but it
		// breaks test which modify the config
		const id = mw.config.get( 'wgNamespaceIds' )[ ns.toLowerCase() ];
		if ( id === undefined ) {
			return false;
		}
		return id;
	},

	/**
	 * @private
	 * @method isKnownNamespace
	 * @param {number} namespace that may or may not exist
	 * @return {boolean}
	 */
	isKnownNamespace = function ( namespace ) {
		return namespace === NS_MAIN || mw.config.get( 'wgFormattedNamespaces' )[ namespace ] !== undefined;
	},

	/**
	 * @private
	 * @method getNamespacePrefix
	 * @param {number} namespace that is valid and known. Callers should call
	 *  `isKnownNamespace` before executing this method.
	 * @return {string}
	 */
	getNamespacePrefix = function ( namespace ) {
		return namespace === NS_MAIN ?
			'' :
			( mw.config.get( 'wgFormattedNamespaces' )[ namespace ].replace( / /g, '_' ) + ':' );
	},

	rUnderscoreTrim = /^_+|_+$/g,

	rSplit = /^(.+?)_*:_*(.*)$/,

	// See MediaWikiTitleCodec.php#getTitleInvalidRegex
	// eslint-disable-next-line security/detect-non-literal-regexp
	rInvalid = new RegExp(
		'[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' +
		// URL percent encoding sequences interfere with the ability
		// to round-trip titles -- you can't link to them consistently.
		'|%[\\dA-Fa-f]{2}' +
		// XML/HTML character references produce similar issues.
		'|&[\\dA-Za-z\u0080-\uFFFF]+;'
	),

	// From MediaWikiTitleCodec::splitTitleString() in PHP
	// Note that this is not equivalent to /\s/, e.g. underscore is included, tab is not included.
	rWhitespace = /[ _\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+/g,

	// From MediaWikiTitleCodec::splitTitleString() in PHP
	rUnicodeBidi = /[\u200E\u200F\u202A-\u202E]+/g,

	/**
	 * Slightly modified from Flinfo. Credit goes to Lupo and Flominator.
	 *
	 * @private
	 * @static
	 * @property {Object[]} sanitationRules
	 */
	sanitationRules = [
		// "signature"
		{
			pattern: /~{3}/g,
			replace: '',
			generalRule: true
		},
		// control characters
		{
			// eslint-disable-next-line no-control-regex
			pattern: /[\x00-\x1f\x7f]/g,
			replace: '',
			generalRule: true
		},
		// URL encoding (possibly)
		{
			pattern: /%([\dA-Fa-f]{2})/g,
			replace: '% $1',
			generalRule: true
		},
		// HTML-character-entities
		{
			pattern: /&(([\dA-Za-z\x80-\xff]+|#\d+|#x[\dA-Fa-f]+);)/g,
			replace: '& $1',
			generalRule: true
		},
		// slash, colon (not supported by file systems like NTFS/Windows, Mac OS 9 [:], ext4 [/])
		{
			// eslint-disable-next-line security/detect-non-literal-regexp
			pattern: new RegExp( '[' + mw.config.get( 'wgIllegalFileChars', '' ) + ']', 'g' ),
			replace: '-',
			fileRule: true
		},
		// brackets, greater than
		{
			pattern: /[}\]>]/g,
			replace: ')',
			generalRule: true
		},
		// brackets, lower than
		{
			pattern: /[{[<]/g,
			replace: '(',
			generalRule: true
		},
		// everything that wasn't covered yet
		{
			// eslint-disable-next-line security/detect-non-literal-regexp
			pattern: new RegExp( rInvalid.source, 'g' ),
			replace: '-',
			generalRule: true
		},
		// directory structures
		{
			pattern: /^(\.|\.\.|\.\/.*|\.\.\/.*|.*\/\.\/.*|.*\/\.\.\/.*|.*\/\.|.*\/\.\.)$/g,
			replace: '',
			generalRule: true
		}
	],

	/**
	 * Internal helper for #constructor and #newFromText.
	 *
	 * Based on Title.php#secureAndSplit
	 *
	 * @private
	 * @static
	 * @method parse
	 * @param {string} title
	 * @param {number} [defaultNamespace=NS_MAIN]
	 * @return {Object|boolean}
	 */
	parse = function ( title, defaultNamespace ) {
		let namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;

		title = title
			// Strip Unicode bidi override characters
			.replace( rUnicodeBidi, '' )
			// Normalise whitespace to underscores and remove duplicates
			.replace( rWhitespace, '_' )
			// Trim underscores
			.replace( rUnderscoreTrim, '' );

		if ( title.indexOf( '\uFFFD' ) !== -1 ) {
			// Contained illegal UTF-8 sequences or forbidden Unicode chars.
			// Commonly occurs when the text was obtained using the `URL` API, and the 'title' parameter
			// was using a legacy 8-bit encoding, for example:
			// new URL( 'https://en.wikipedia.org/w/index.php?title=Apollo%96Soyuz' ).searchParams.get( 'title' )
			return false;
		}

		// Process initial colon
		if ( title !== '' && title[ 0 ] === ':' ) {
			// Initial colon means main namespace instead of specified default
			namespace = NS_MAIN;
			title = title
				// Strip colon
				.slice( 1 )
				// Trim underscores
				.replace( rUnderscoreTrim, '' );
		}

		if ( title === '' ) {
			return false;
		}

		// Process namespace prefix (if any)
		let m = title.match( rSplit );
		if ( m ) {
			const id = getNsIdByName( m[ 1 ] );
			if ( id !== false ) {
				// Ordinary namespace
				namespace = id;
				title = m[ 2 ];

				// For Talk:X pages, make sure X has no "namespace" prefix
				if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) {
					// Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x)
					if ( getNsIdByName( m[ 1 ] ) !== false ) {
						return false;
					}
				}
			}
		}

		// Process fragment
		const i = title.indexOf( '#' );
		let fragment;
		if ( i === -1 ) {
			fragment = null;
		} else {
			fragment = title
				// Get segment starting after the hash
				.slice( i + 1 )
				// Convert to text
				// NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo")
				.replace( /_/g, ' ' );

			title = title
				// Strip hash
				.slice( 0, i )
				// Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux")
				.replace( rUnderscoreTrim, '' );
		}

		// Reject illegal characters
		if ( rInvalid.test( title ) ) {
			return false;
		}

		// Disallow titles that browsers or servers might resolve as directory navigation
		if (
			title.indexOf( '.' ) !== -1 && (
				title === '.' || title === '..' ||
				title.indexOf( './' ) === 0 ||
				title.indexOf( '../' ) === 0 ||
				title.indexOf( '/./' ) !== -1 ||
				title.indexOf( '/../' ) !== -1 ||
				title.slice( -2 ) === '/.' ||
				title.slice( -3 ) === '/..'
			)
		) {
			return false;
		}

		// Disallow magic tilde sequence
		if ( title.indexOf( '~~~' ) !== -1 ) {
			return false;
		}

		// Disallow titles exceeding the TITLE_MAX_BYTES byte size limit (size of underlying database field)
		// Except for special pages, e.g. [[Special:Block/Long name]]
		// Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should
		// be less than 512 bytes.
		if ( namespace !== NS_SPECIAL && mwString.byteLength( title ) > TITLE_MAX_BYTES ) {
			return false;
		}

		// Can't make a link to a namespace alone.
		if ( title === '' && namespace !== NS_MAIN ) {
			return false;
		}

		// Any remaining initial :s are illegal.
		if ( title[ 0 ] === ':' ) {
			return false;
		}

		return {
			namespace: namespace,
			title: title,
			fragment: fragment
		};
	},

	/**
	 * Convert db-key to readable text.
	 *
	 * @private
	 * @static
	 * @method text
	 * @param {string} s
	 * @return {string}
	 */
	text = function ( s ) {
		return s.replace( /_/g, ' ' );
	},

	/**
	 * Sanitizes a string based on a rule set and a filter
	 *
	 * @private
	 * @static
	 * @method sanitize
	 * @param {string} s
	 * @param {Array} filter
	 * @return {string}
	 */
	sanitize = function ( s, filter ) {
		const rules = sanitationRules;

		for ( let i = 0, ruleLength = rules.length; i < ruleLength; ++i ) {
			const rule = rules[ i ];
			for ( let m = 0, filterLength = filter.length; m < filterLength; ++m ) {
				if ( rule[ filter[ m ] ] ) {
					s = s.replace( rule.pattern, rule.replace );
				}
			}
		}
		return s;
	},

	/**
	 * Cuts a string to a specific byte length, assuming UTF-8
	 * or less, if the last character is a multi-byte one
	 *
	 * @private
	 * @static
	 * @method trimToByteLength
	 * @param {string} s
	 * @param {number} length
	 * @return {string}
	 */
	trimToByteLength = function ( s, length ) {
		return mwString.trimByteLength( '', s, length ).newVal;
	},

	/**
	 * Cuts a file name to a specific byte length
	 *
	 * @private
	 * @static
	 * @method trimFileNameToByteLength
	 * @param {string} name without extension
	 * @param {string} extension file extension
	 * @return {string} The full name, including extension
	 */
	trimFileNameToByteLength = function ( name, extension ) {
		// There is a special byte limit for file names and ... remember the dot
		return trimToByteLength( name, FILENAME_MAX_BYTES - extension.length - 1 ) + '.' + extension;
	};

/**
 * @class mw.Title
 * @classdesc Library for constructing MediaWiki titles.
 *
 * @example
 * new mw.Title( 'Foo', NS_TEMPLATE ).getPrefixedText();
 * // => 'Template:Foo'
 * mw.Title.newFromText( 'Foo', NS_TEMPLATE ).getPrefixedText();
 * // => 'Template:Foo'
 * mw.Title.makeTitle( NS_TEMPLATE, 'Foo' ).getPrefixedText();
 * // => 'Template:Foo'
 *
 * new mw.Title( 'Category:Foo', NS_TEMPLATE ).getPrefixedText();
 * // => 'Category:Foo'
 * mw.Title.newFromText( 'Category:Foo', NS_TEMPLATE ).getPrefixedText();
 * // => 'Category:Foo'
 * mw.Title.makeTitle( NS_TEMPLATE, 'Category:Foo' ).getPrefixedText();
 * // => 'Template:Category:Foo'
 *
 * new mw.Title( 'Template:Foo', NS_TEMPLATE ).getPrefixedText();
 * // => 'Template:Foo'
 * mw.Title.newFromText( 'Template:Foo', NS_TEMPLATE ).getPrefixedText();
 * // => 'Template:Foo'
 * mw.Title.makeTitle( NS_TEMPLATE, 'Template:Foo' ).getPrefixedText();
 * // => 'Template:Template:Foo'
 *
 * @constructor
 * @description Parse titles into an object structure. Note that when using the constructor
 * directly, passing invalid titles will result in an exception.
 * Use [newFromText]{@link mw.Title.newFromText} to use the
 * logic directly and get null for invalid titles which is easier to work with.
 *
 * Note that in the constructor and [newFromText]{@link mw.Title.newFromText} method,
 * `namespace` is the **default** namespace only, and can be overridden by a namespace
 * prefix in `title`. If you do not want this behavior,
 * use [makeTitle]{@link mw.Title.makeTitle}.
 *
 * @param {string} title Title of the page. If no second argument given,
 *  this will be searched for a namespace
 * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title
 * @throws {Error} When the title is invalid
 */
function Title( title, namespace ) {
	const parsed = parse( title, namespace );
	if ( !parsed ) {
		throw new Error( 'Unable to parse title' );
	}

	this.namespace = parsed.namespace;
	this.title = parsed.title;
	this.fragment = parsed.fragment;
}

/* Static members */

/**
 * Constructor for Title objects with a null return instead of an exception for invalid titles.
 *
 * Note that `namespace` is the **default** namespace only, and can be overridden by a namespace
 * prefix in `title`. If you do not want this behavior, use #makeTitle. See #constructor for
 * details.
 *
 * @name mw.Title.newFromText
 * @method
 * @param {string} title
 * @param {number} [namespace=NS_MAIN] Default namespace
 * @return {mw.Title|null} A valid Title object or null if the title is invalid
 */
Title.newFromText = function ( title, namespace ) {
	const parsed = parse( title, namespace );
	if ( !parsed ) {
		return null;
	}

	const t = Object.create( Title.prototype );
	t.namespace = parsed.namespace;
	t.title = parsed.title;
	t.fragment = parsed.fragment;

	return t;
};

/**
 * Constructor for Title objects with predefined namespace.
 *
 * Unlike [newFromText]{@link mw.Title.newFromText} or the constructor, this function doesn't allow the given `namespace` to be
 * overridden by a namespace prefix in `title`. See the constructor documentation for details about this behavior.
 *
 * The single exception to this is when `namespace` is 0, indicating the main namespace. The
 * function behaves like [newFromText]{@link mw.Title.newFromText} in that case.
 *
 * @name mw.Title.makeTitle
 * @method
 * @param {number} namespace Namespace to use for the title
 * @param {string} title
 * @return {mw.Title|null} A valid Title object or null if the title is invalid
 */
Title.makeTitle = function ( namespace, title ) {
	if ( !isKnownNamespace( namespace ) ) {
		return null;
	} else {
		return mw.Title.newFromText( getNamespacePrefix( namespace ) + title );
	}
};

/**
 * Constructor for Title objects from user input altering that input to
 * produce a title that MediaWiki will accept as legal.
 *
 * @name mw.Title.newFromUserInput
 * @method
 * @param {string} title
 * @param {number} [defaultNamespace=NS_MAIN]
 *  If given, will used as default namespace for the given title.
 * @param {Object} [options] additional options
 * @param {boolean} [options.forUploading=true]
 *  Makes sure that a file is uploadable under the title returned.
 *  There are pages in the file namespace under which file upload is impossible.
 *  Automatically assumed if the title is created in the Media namespace.
 * @return {mw.Title|null} A valid Title object or null if the input cannot be turned into a valid title
 */
Title.newFromUserInput = function ( title, defaultNamespace, options ) {
	let namespace = parseInt( defaultNamespace ) || NS_MAIN;

	// merge options into defaults
	options = $.extend( {
		forUploading: true
	}, options );

	// Normalise additional whitespace
	title = title.replace( /\s/g, ' ' ).trim();

	// Process initial colon
	if ( title !== '' && title[ 0 ] === ':' ) {
		// Initial colon means main namespace instead of specified default
		namespace = NS_MAIN;
		title = title
			// Strip colon
			.slice( 1 )
			// Trim underscores
			.replace( rUnderscoreTrim, '' );
	}

	// Process namespace prefix (if any)
	const m = title.match( rSplit );
	if ( m ) {
		const id = getNsIdByName( m[ 1 ] );
		if ( id !== false ) {
			// Ordinary namespace
			namespace = id;
			title = m[ 2 ];
		}
	}

	if (
		namespace === NS_MEDIA ||
		( options.forUploading && ( namespace === NS_FILE ) )
	) {
		title = sanitize( title, [ 'generalRule', 'fileRule' ] );

		// Operate on the file extension
		// Although it is possible having spaces between the name and the ".ext" this isn't nice for
		// operating systems hiding file extensions -> strip them later on
		const lastDot = title.lastIndexOf( '.' );

		// No or empty file extension
		if ( lastDot === -1 || lastDot >= title.length - 1 ) {
			return null;
		}

		// Get the last part, which is supposed to be the file extension
		const ext = title.slice( lastDot + 1 );

		// Remove whitespace of the name part (that without extension)
		title = title.slice( 0, lastDot ).trim();

		// Cut, if too long and append file extension
		title = trimFileNameToByteLength( title, ext );
	} else {
		title = sanitize( title, [ 'generalRule' ] );

		// Cut titles exceeding the TITLE_MAX_BYTES byte size limit
		// (size of underlying database field)
		if ( namespace !== NS_SPECIAL ) {
			title = trimToByteLength( title, TITLE_MAX_BYTES );
		}
	}

	// Any remaining initial :s are illegal.
	title = title.replace( /^:+/, '' );

	return Title.newFromText( title, namespace );
};

/**
 * Sanitizes a file name as supplied by the user, originating in the user's file system
 * so it is most likely a valid MediaWiki title and file name after processing.
 * Returns null on fatal errors.
 *
 * @name mw.Title.newFromFileName
 * @method
 * @param {string} uncleanName The unclean file name including file extension but
 *   without namespace
 * @return {mw.Title|null} A valid Title object or null if the title is invalid
 */
Title.newFromFileName = function ( uncleanName ) {
	return Title.newFromUserInput( 'File:' + uncleanName );
};

/**
 * Get the file title from an image element.
 *
 * @example
 * const title = mw.Title.newFromImg( imageNode );
 *
 * @name mw.Title.newFromImg
 * @method
 * @param {HTMLElement|jQuery} img The image to use as a base
 * @return {mw.Title|null} The file title or null if unsuccessful
 */
Title.newFromImg = function ( img ) {
	const src = img.jquery ? img[ 0 ].src : img.src,
		data = mw.util.parseImageUrl( src );

	return data ? mw.Title.newFromText( 'File:' + data.name ) : null;
};

/**
 * Check if a given namespace is a talk namespace.
 *
 * See NamespaceInfo::isTalk in PHP
 *
 * @name mw.Title.isTalkNamespace
 * @method
 * @param {number} namespaceId Namespace ID
 * @return {boolean} Namespace is a talk namespace
 */
Title.isTalkNamespace = function ( namespaceId ) {
	return namespaceId > NS_MAIN && namespaceId % 2 === 1;
};

/**
 * Check if signature buttons should be shown in a given namespace.
 *
 * See NamespaceInfo::wantSignatures in PHP
 *
 * @name mw.Title.wantSignaturesNamespace
 * @method
 * @param {number} namespaceId Namespace ID
 * @return {boolean} Namespace is a signature namespace
 */
Title.wantSignaturesNamespace = function ( namespaceId ) {
	return Title.isTalkNamespace( namespaceId ) ||
		mw.config.get( 'wgExtraSignatureNamespaces' ).indexOf( namespaceId ) !== -1;
};

/**
 * Whether this title exists on the wiki.
 *
 * @name mw.Title.exists
 * @method
 * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title
 * @return {boolean|null} Boolean if the information is available, otherwise null
 * @throws {Error} If title is not a string or mw.Title
 */
Title.exists = function ( title ) {
	const obj = Title.exist.pages;

	let match;
	if ( typeof title === 'string' ) {
		match = obj[ title ];
	} else if ( title instanceof Title ) {
		match = obj[ title.toString() ];
	} else {
		throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' );
	}

	if ( typeof match !== 'boolean' ) {
		return null;
	}

	return match;
};

/**
 * @typedef {Object} mw.Title~TitleExistenceStore
 * @property {Object} pages Keyed by title. Boolean true value indicates page does exist.
 *
 * @property {Function} set The setter function. Returns a boolean.
 *
 * Example to declare existing titles:
 * ```
 * Title.exist.set( ['User:John_Doe', ...] );
 * ```
 *
 * Example to declare titles nonexistent:
 * ```
 * Title.exist.set( ['File:Foo_bar.jpg', ...], false );
 * ```
 *
 * @property {string|string[]} set.titles Title(s) in strict prefixedDb title form
 * @property {boolean} [set.state=true] State of the given titles
 */

/**
 * @name mw.Title.exist
 * @type {mw.Title~TitleExistenceStore}
 */
Title.exist = {
	pages: {},

	set: function ( titles, state ) {
		const pages = this.pages;

		titles = Array.isArray( titles ) ? titles : [ titles ];
		state = state === undefined ? true : !!state;

		for ( let i = 0, len = titles.length; i < len; i++ ) {
			pages[ titles[ i ] ] = state;
		}
		return true;
	}
};

/**
 * Normalize a file extension to the common form, making it lowercase and checking some synonyms,
 * and ensure it's clean. Extensions with non-alphanumeric characters will be discarded.
 * Keep in sync with File::normalizeExtension() in PHP.
 *
 * @name mw.Title.normalizeExtension
 * @method
 * @param {string} extension File extension (without the leading dot)
 * @return {string} File extension in canonical form
 */
Title.normalizeExtension = function ( extension ) {
	const
		lower = extension.toLowerCase(),
		normalizations = {
			htm: 'html',
			jpeg: 'jpg',
			mpeg: 'mpg',
			tiff: 'tif',
			ogv: 'ogg'
		};
	if ( Object.hasOwnProperty.call( normalizations, lower ) ) {
		return normalizations[ lower ];
	} else if ( /^[\da-z]+$/.test( lower ) ) {
		return lower;
	} else {
		return '';
	}
};

/**
 * PHP's strtoupper differs from String.toUpperCase in a number of cases (T147646).
 *
 * @name mw.Title.phpCharToUpper
 * @method
 * @param {string} chr Unicode character
 * @return {string} Unicode character, in upper case, according to the same rules as in PHP
 */
Title.phpCharToUpper = function ( chr ) {
	if ( !toUpperMap ) {
		toUpperMap = require( './phpCharToUpper.json' );
	}
	if ( toUpperMap[ chr ] === 0 ) {
		// Optimisation: When the override is to keep the character unchanged,
		// we use 0 in JSON. This reduces the data by 50%.
		return chr;
	}
	return toUpperMap[ chr ] || chr.toUpperCase();
};

/* Public members */
Title.prototype = /** @lends mw.Title.prototype */ {
	constructor: Title,

	/**
	 * Get the namespace number.
	 *
	 * Example: 6 for "File:Example_image.svg".
	 *
	 * @return {number}
	 */
	getNamespaceId: function () {
		return this.namespace;
	},

	/**
	 * Get the namespace prefix (in the content language).
	 *
	 * Example: "File:" for "File:Example_image.svg".
	 * In #NS_MAIN this is '', otherwise namespace name plus ':'
	 *
	 * @return {string}
	 */
	getNamespacePrefix: function () {
		return getNamespacePrefix( this.namespace );
	},

	/**
	 * Get the page name as if it is a file name, without extension or namespace prefix,
	 * in the canonical form with underscores instead of spaces. For example, the title
	 * `File:Example_image.svg` will be returned as `Example_image`.
	 *
	 * Note that this method will work for non-file titles but probably give nonsensical results.
	 * A title like `User:Dr._J._Fail` will be returned as `Dr._J`! Use [getMain]{@link mw.Title#getMain} instead.
	 *
	 * @return {string}
	 */
	getFileNameWithoutExtension: function () {
		const ext = this.getExtension();
		if ( ext === null ) {
			return this.getMain();
		}
		return this.getMain().slice( 0, -ext.length - 1 );
	},

	/**
	 * Get the page name as if it is a file name, without extension or namespace prefix,
	 * in the human-readable form with spaces instead of underscores. For example, the title
	 * `File:Example_image.svg` will be returned as "Example image".
	 *
	 * Note that this method will work for non-file titles but probably give nonsensical results.
	 * A title like `User:Dr._J._Fail` will be returned as `Dr. J`! Use [getMainText]{@link mw.Title#getMainText} instead.
	 *
	 * @return {string}
	 */
	getFileNameTextWithoutExtension: function () {
		return text( this.getFileNameWithoutExtension() );
	},

	/**
	 * Get the page name as if it is a file name, without extension or namespace prefix. Warning,
	 * this is usually not what you want! A title like `User:Dr._J._Fail` will be returned as
	 * `Dr. J`! Use [getMain]{@link mw.Title#getMain} or [getMainText]{@link mw.Title#getMainText} for the actual page name.
	 *
	 * @return {string} File name without file extension, in the canonical form with underscores
	 *  instead of spaces. For example, the title `File:Example_image.svg` will be returned as
	 *  `Example_image`.
	 *  @deprecated since 1.40, use [getFileNameWithoutExtension]{@link mw.Title#getFileNameWithoutExtension} instead
	 */
	getName: function () {
		return this.getFileNameWithoutExtension();
	},

	/**
	 * Get the page name as if it is a file name, without extension or namespace prefix. Warning,
	 * this is usually not what you want! A title like `User:Dr._J._Fail` will be returned as
	 * `Dr. J`! Use [getMainText]{@link mw.Title#getMainText} for the actual page name.
	 *
	 * @return {string} File name without file extension, formatted with spaces instead of
	 *  underscores. For example, the title `File:Example_image.svg` will be returned as
	 *  `Example image`.
	 *  @deprecated since 1.40, use [getFileNameTextWithoutExtension]{@link mw.Title#getFileNameTextWithoutExtension} instead
	 */
	getNameText: function () {
		return text( this.getFileNameTextWithoutExtension() );
	},

	/**
	 * Get the extension of the page name (if any).
	 *
	 * @return {string|null} Name extension or null if there is none
	 */
	getExtension: function () {
		const lastDot = this.title.lastIndexOf( '.' );
		if ( lastDot === -1 ) {
			return null;
		}
		return this.title.slice( lastDot + 1 ) || null;
	},

	/**
	 * Get the main page name.
	 *
	 * Example: `Example_image.svg` for `File:Example_image.svg`.
	 *
	 * @return {string}
	 */
	getMain: function () {
		if (
			mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( this.namespace ) !== -1 ||
			!this.title.length
		) {
			return this.title;
		}
		const firstChar = mwString.charAt( this.title, 0 );
		return mw.Title.phpCharToUpper( firstChar ) + this.title.slice( firstChar.length );
	},

	/**
	 * Get the main page name (transformed by text()).
	 *
	 * Example: `Example image.svg` for `File:Example_image.svg`.
	 *
	 * @return {string}
	 */
	getMainText: function () {
		return text( this.getMain() );
	},

	/**
	 * Get the full page name.
	 *
	 * Example: `File:Example_image.svg`.
	 * Most useful for API calls, anything that must identify the "title".
	 *
	 * @return {string}
	 */
	getPrefixedDb: function () {
		return this.getNamespacePrefix() + this.getMain();
	},

	/**
	 * Get the full page name (transformed by [text]{@link mw.Title#text}).
	 *
	 * Example: `File:Example image.svg` for `File:Example_image.svg`.
	 *
	 * @return {string}
	 */
	getPrefixedText: function () {
		return text( this.getPrefixedDb() );
	},

	/**
	 * Get the page name relative to a namespace.
	 *
	 * Example:
	 *
	 * - "Foo:Bar" relative to the Foo namespace becomes "Bar".
	 * - "Bar" relative to any non-main namespace becomes ":Bar".
	 * - "Foo:Bar" relative to any namespace other than Foo stays "Foo:Bar".
	 *
	 * @param {number} namespace The namespace to be relative to
	 * @return {string}
	 */
	getRelativeText: function ( namespace ) {
		if ( this.getNamespaceId() === namespace ) {
			return this.getMainText();
		} else if ( this.getNamespaceId() === NS_MAIN ) {
			return ':' + this.getPrefixedText();
		} else {
			return this.getPrefixedText();
		}
	},

	/**
	 * Get the fragment (if any).
	 *
	 * Note that this method (by design) does not include the hash character and
	 * the value is not url encoded.
	 *
	 * @return {string|null}
	 */
	getFragment: function () {
		return this.fragment;
	},

	/**
	 * Get the URL to this title.
	 *
	 * @see [mw.util.getUrl]{@link module:mediawiki.util.getUrl}
	 * @param {Object} [params] A mapping of query parameter names to values,
	 *     e.g. `{ action: 'edit' }`.
	 * @return {string}
	 */
	getUrl: function ( params ) {
		const fragment = this.getFragment();
		if ( fragment ) {
			return mw.util.getUrl( this.toString() + '#' + fragment, params );
		} else {
			return mw.util.getUrl( this.toString(), params );
		}
	},

	/**
	 * Check if the title is in a talk namespace.
	 *
	 * @return {boolean} The title is in a talk namespace
	 */
	isTalkPage: function () {
		return Title.isTalkNamespace( this.getNamespaceId() );
	},

	/**
	 * Get the title for the associated talk page.
	 *
	 * @return {mw.Title|null} The title for the associated talk page, null if not available
	 */
	getTalkPage: function () {
		if ( !this.canHaveTalkPage() ) {
			return null;
		}
		return this.isTalkPage() ?
			this :
			Title.makeTitle( this.getNamespaceId() + 1, this.getMainText() );
	},

	/**
	 * Get the title for the subject page of a talk page.
	 *
	 * @return {mw.Title|null} The title for the subject page of a talk page, null if not available
	 */
	getSubjectPage: function () {
		return this.isTalkPage() ?
			Title.makeTitle( this.getNamespaceId() - 1, this.getMainText() ) :
			this;
	},

	/**
	 * Check the title can have an associated talk page.
	 *
	 * @return {boolean} The title can have an associated talk page
	 */
	canHaveTalkPage: function () {
		return this.getNamespaceId() >= NS_MAIN;
	},

	/**
	 * Whether this title exists on the wiki.
	 *
	 * @see mw.Title.exists
	 * @return {boolean|null} Boolean if the information is available, otherwise null
	 */
	exists: function () {
		return Title.exists( this );
	}
};

/**
 * Alias of [getPrefixedDb]{@link mw.Title#getPrefixedDb}.
 *
 * @name mw.Title.prototype.toString
 * @method
 */
Title.prototype.toString = Title.prototype.getPrefixedDb;

/**
 * Alias of [getPrefixedText]{@link mw.Title#getPrefixedText}.
 *
 * @name mw.Title.prototype.toText
 * @method
 */
Title.prototype.toText = Title.prototype.getPrefixedText;

// Expose
mw.Title = Title;